├── .eslintrc ├── .gitignore ├── .npmignore ├── .stylintrc ├── .travis.yml ├── LICENSE ├── README.md ├── babel.config.js ├── docs ├── 0b39d1e64af23fe5437238402d08496a.svg ├── 35071d00819547a959ef3450c129d77e.eot ├── 37f4597594857b017901209aae0a60e1.svg ├── 3eff5a4e9fb92ca96cbf2fa77649b8c1.ttf ├── 81436770636b45508203b3022075ae73.ttf ├── 8a53d21a4d9aa1aac2bf15093bd748c4.woff ├── af39e41be700d6148c61a6c1ffc84215.svg ├── build │ └── bundle.55aecd75.js ├── bundle.js ├── bundle.js.map ├── d6c7e9d3e5adb7a5261c5ad9f7d3caaa.woff ├── d9c6d360d27eac625da0405245ec9f0d.eot └── index.html ├── package.json ├── setupTests.js ├── src ├── Repeatable.jsx └── index.js ├── styleguide.config.js ├── styleguide ├── components │ ├── Progress.jsx │ ├── StyleGuideRenderer.jsx │ └── Wrapper.jsx ├── examples │ └── README.md ├── setup.js └── styles.css ├── test └── index.js └── webpack.config.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "trendmicro", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "node": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | package-lock.json 4 | /.nyc_output 5 | /coverage 6 | /dist 7 | /lib 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | /.nyc_output 3 | /coverage 4 | -------------------------------------------------------------------------------- /.stylintrc: -------------------------------------------------------------------------------- 1 | // 2 | // https://github.com/rossPatton/stylint 3 | // 4 | { 5 | "blocks": false, 6 | "brackets": "always", 7 | "colons": "always", 8 | "colors": false, 9 | "commaSpace": "always", 10 | "commentSpace": false, 11 | "cssLiteral": "never", 12 | "depthLimit": false, 13 | "duplicates": false, 14 | "efficient": "always", 15 | "extendPref": false, 16 | "globalDupe": false, 17 | "indentPref": false, 18 | "leadingZero": "never", 19 | "maxErrors": false, 20 | "maxWarnings": false, 21 | "mixed": false, 22 | "namingConvention": false, 23 | "namingConventionStrict": false, 24 | "none": "never", 25 | "noImportant": true, 26 | "parenSpace": false, 27 | "placeholders": "always", 28 | "prefixVarsWithDollar": "always", 29 | "quotePref": false, 30 | "semicolons": "always", 31 | "sortOrder": false, 32 | "stackedProperties": "never", 33 | "trailingWhitespace": "never", 34 | "universal": false, 35 | "valid": true, 36 | "zeroUnits": "never", 37 | "zIndexNormalize": false 38 | } 39 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | group: edge 4 | 5 | language: node_js 6 | 7 | os: 8 | - linux 9 | 10 | node_js: 11 | - '8' 12 | - '10' 13 | 14 | before_install: 15 | - npm install -g npm 16 | - npm --version 17 | 18 | after_success: 19 | - npm run coveralls 20 | - npm run coverage-clean 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present Cheton Wu 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 | # react-repeatable [![build status](https://travis-ci.org/cheton/react-repeatable.svg?branch=master)](https://travis-ci.org/cheton/react-repeatable) [![Coverage Status](https://coveralls.io/repos/github/cheton/react-repeatable/badge.svg?branch=master)](https://coveralls.io/github/cheton/react-repeatable?branch=master) 2 | 3 | [![NPM](https://nodei.co/npm/react-repeatable.png?downloads=true&stars=true)](https://nodei.co/npm/react-repeatable/) 4 | 5 | A press and hold wrapper component that can trigger hold action multiple times while holding down. 6 | 7 | Demo: https://cheton.github.io/react-repeatable 8 | 9 | ## Installation 10 | 11 | ``` 12 | npm install --save react-repeatable 13 | ``` 14 | 15 | ## Usage 16 | 17 | ```jsx 18 | { 22 | // Callback fired when the mousedown or touchstart event is triggered. 23 | }} 24 | onHoldStart={() => { 25 | // Callback fired once before the first hold action. 26 | }} 27 | onHold={() => { 28 | // Callback fired mutiple times while holding down. 29 | }} 30 | onHoldEnd={() => { 31 | // Callback fired once after the last hold action. 32 | }} 33 | onRelease={(event) => { 34 | // Callback fired when the mouseup, touchcancel, or touchend event is triggered. 35 | }} 36 | > 37 | Press Me 38 | 39 | ``` 40 | 41 | ### Repeatable Button 42 | 43 | ```jsx 44 | const RepeatableButton = ({ onClick, ...props }) => ( 45 | 52 | ); 53 | 54 | 55 | ``` 56 | 57 | ## API 58 | 59 | ### Sequence of Events 60 | 61 | #### Hold action is occurred 62 | onPress -> onHoldStart -> onHold (once or more) -> onHoldEnd -> onRelease 63 | 64 | #### Hold action is not occurred 65 | onPress -> onRelease 66 | 67 | ### Properties 68 | 69 | Name | Type | Default | Description 70 | :--- | :--- | :------ | :---------- 71 | tag | element | 'div' | A custom element for this component. 72 | disabled | Boolean | false | Set it to true to disable event actions. 73 | repeatDelay | Number | 500 | The time (in milliseconds) to wait before the first hold action is being triggered. 74 | repeatInterval | Number | 32 | The time interval (in milliseconds) on how often to trigger a hold action. 75 | repeatCount | Number | 0 | The number of times the hold action will take place. A zero value will disable the repeat counter. 76 | onPress | Function(event) | | Callback fired when the mousedown or touchstart event is triggered. 77 | onHoldStart | Function() | | Callback fired once before the first hold action. 78 | onHold | Function() | | Callback fired mutiple times while holding down. 79 | onHoldEnd | Function() | | Callback fired once after the last hold action. 80 | onRelease | Function(event) | | Callback fired when the mouseup, touchcancel, or touchend event is triggered. 81 | 82 | ## License 83 | 84 | MIT 85 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '@trendmicro/babel-config', 3 | presets: [ 4 | '@babel/preset-env', 5 | '@babel/preset-react' 6 | ] 7 | }; 8 | -------------------------------------------------------------------------------- /docs/35071d00819547a959ef3450c129d77e.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheton/react-repeatable/d3dfba4114992e9f97a41f095df41514b17bf578/docs/35071d00819547a959ef3450c129d77e.eot -------------------------------------------------------------------------------- /docs/37f4597594857b017901209aae0a60e1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/3eff5a4e9fb92ca96cbf2fa77649b8c1.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheton/react-repeatable/d3dfba4114992e9f97a41f095df41514b17bf578/docs/3eff5a4e9fb92ca96cbf2fa77649b8c1.ttf -------------------------------------------------------------------------------- /docs/81436770636b45508203b3022075ae73.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheton/react-repeatable/d3dfba4114992e9f97a41f095df41514b17bf578/docs/81436770636b45508203b3022075ae73.ttf -------------------------------------------------------------------------------- /docs/8a53d21a4d9aa1aac2bf15093bd748c4.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheton/react-repeatable/d3dfba4114992e9f97a41f095df41514b17bf578/docs/8a53d21a4d9aa1aac2bf15093bd748c4.woff -------------------------------------------------------------------------------- /docs/d6c7e9d3e5adb7a5261c5ad9f7d3caaa.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheton/react-repeatable/d3dfba4114992e9f97a41f095df41514b17bf578/docs/d6c7e9d3e5adb7a5261c5ad9f7d3caaa.woff -------------------------------------------------------------------------------- /docs/d9c6d360d27eac625da0405245ec9f0d.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheton/react-repeatable/d3dfba4114992e9f97a41f095df41514b17bf578/docs/d9c6d360d27eac625da0405245ec9f0d.eot -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | React Component v2.0.0
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-repeatable", 3 | "version": "2.0.1", 4 | "description": "A press and hold wrapper component that can trigger hold action multiple times while holding down.", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "lib" 8 | ], 9 | "scripts": { 10 | "prepare": "npm run lint && npm test && npm run clean && npm run build && npm run styleguide:build", 11 | "build": "webpack-cli", 12 | "clean": "rm -f {lib,dist}/*", 13 | "demo": "http-server -p 8000 docs/", 14 | "lint": "npm run eslint", 15 | "eslint": "eslint --ext .js --ext .jsx *.js src test", 16 | "test": "tap test/*.js --node-arg=--require --node-arg=@babel/register --node-arg=--require --node-arg=@babel/polyfill", 17 | "coveralls": "tap test/*.js --coverage --coverage-report=text-lcov --nyc-arg=--require --nyc-arg=@babel/register --nyc-arg=--require --nyc-arg=@babel/polyfill | coveralls", 18 | "dev": "npm run styleguide", 19 | "styleguide": "styleguidist server", 20 | "styleguide:build": "styleguidist build" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/cheton/react-repeatable.git" 25 | }, 26 | "author": "Cheton Wu ", 27 | "contributors": [ 28 | { 29 | "name": "Cheton Wu", 30 | "email": "cheton@gmail.com", 31 | "url": "https://github.com/cheton" 32 | } 33 | ], 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/cheton/react-repeatable/issues" 37 | }, 38 | "homepage": "https://github.com/cheton/react-repeatable", 39 | "keywords": [ 40 | "react", 41 | "repeatable", 42 | "press", 43 | "hold", 44 | "release", 45 | "click" 46 | ], 47 | "peerDependencies": { 48 | "react": "^0.14.0 || >=15.0.0" 49 | }, 50 | "dependencies": { 51 | "chained-function": "^0.5.0", 52 | "prop-types": "^15.6.0" 53 | }, 54 | "devDependencies": { 55 | "@babel/cli": "~7.5.5", 56 | "@babel/core": "~7.5.5", 57 | "@babel/polyfill": "~7.4.4", 58 | "@babel/preset-env": "~7.5.5", 59 | "@babel/preset-react": "~7.0.0", 60 | "@babel/register": "~7.5.5", 61 | "@trendmicro/babel-config": "~1.0.0-alpha", 62 | "babel-eslint": "~10.0.2", 63 | "babel-loader": "~8.0.6", 64 | "classnames": "~2.2.6", 65 | "coveralls": "~3.0.5", 66 | "cross-env": "~5.2.0", 67 | "css-loader": "~3.1.0", 68 | "enzyme": "~3.10.0", 69 | "enzyme-adapter-react-16": "~1.14.0", 70 | "eslint": "~6.0.1", 71 | "eslint-config-trendmicro": "~1.4.1", 72 | "eslint-loader": "~2.2.1", 73 | "eslint-plugin-import": "~2.18.1", 74 | "eslint-plugin-jsx-a11y": "~6.2.3", 75 | "eslint-plugin-react": "~7.14.2", 76 | "find-imports": "~1.1.0", 77 | "html-webpack-plugin": "~3.2.0", 78 | "http-server": "~0.11.1", 79 | "jsdom": "~15.1.1", 80 | "rc-slider": "~8.6.13", 81 | "react": "~16.8.0", 82 | "react-bootstrap-buttons": "~0.5.0", 83 | "react-dom": "~16.8.0", 84 | "react-github-corner": "~2.3.0", 85 | "react-styleguidist": "~9.1.11", 86 | "sinon": "~7.3.2", 87 | "style-loader": "~0.23.1", 88 | "styled-components": "~4.3.2", 89 | "tap": "~14.4.2", 90 | "webpack": "~4.36.1", 91 | "webpack-cli": "~3.3.6", 92 | "webpack-dev-server": "~3.7.2" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /setupTests.js: -------------------------------------------------------------------------------- 1 | import Enzyme from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | import { JSDOM } from 'jsdom'; 4 | 5 | // React 16 Enzyme adapter 6 | Enzyme.configure({ adapter: new Adapter() }); 7 | 8 | // Ignore `.styl` files 9 | require.extensions['.styl'] = () => { 10 | return; 11 | }; 12 | 13 | // JSDOM 14 | const jsdom = new JSDOM(''); 15 | const { window } = jsdom; 16 | 17 | const copyProps = (src, target) => { 18 | const props = Object.getOwnPropertyNames(src) 19 | .filter(prop => typeof target[prop] === 'undefined') 20 | .reduce((result, prop) => ({ 21 | ...result, 22 | [prop]: Object.getOwnPropertyDescriptor(src, prop), 23 | }), {}); 24 | Object.defineProperties(target, props); 25 | }; 26 | 27 | global.window = window; 28 | global.document = window.document; 29 | global.navigator = { 30 | userAgent: 'node.js', 31 | }; 32 | 33 | copyProps(window, global); 34 | -------------------------------------------------------------------------------- /src/Repeatable.jsx: -------------------------------------------------------------------------------- 1 | import chainedFunction from 'chained-function'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | 5 | class Repeatable extends React.Component { 6 | static propTypes = { 7 | // A custom element for this component. 8 | tag: PropTypes.oneOfType([ 9 | PropTypes.func, 10 | PropTypes.string, 11 | PropTypes.shape({ $$typeof: PropTypes.symbol, render: PropTypes.func }), 12 | PropTypes.arrayOf(PropTypes.oneOfType([ 13 | PropTypes.func, 14 | PropTypes.string, 15 | PropTypes.shape({ $$typeof: PropTypes.symbol, render: PropTypes.func }), 16 | ])) 17 | ]), 18 | 19 | // Set it to true to disable event actions. 20 | disabled: PropTypes.bool, 21 | 22 | // The time (in milliseconds) to wait before the first hold action is being triggered. 23 | repeatDelay: PropTypes.oneOfType([ 24 | PropTypes.number, 25 | PropTypes.string 26 | ]), 27 | 28 | // The time interval (in milliseconds) on how often to trigger a hold action. 29 | repeatInterval: PropTypes.oneOfType([ 30 | PropTypes.number, 31 | PropTypes.string 32 | ]), 33 | 34 | // The number of times the hold action will take place. A zero value will disable the repeat counter. 35 | repeatCount: PropTypes.oneOfType([ 36 | PropTypes.number, 37 | PropTypes.string 38 | ]), 39 | 40 | // Callback fired when the mousedown or touchstart event is triggered. 41 | onPress: PropTypes.func, 42 | 43 | // Callback fired once before the first hold action. 44 | onHoldStart: PropTypes.func, 45 | 46 | // Callback fired mutiple times while holding down. 47 | onHold: PropTypes.func, 48 | 49 | // Callback fired once after the last hold action. 50 | onHoldEnd: PropTypes.func, 51 | 52 | // Callback fired when the mouseup, touchcancel, or touchend event is triggered. 53 | onRelease: PropTypes.func, 54 | 55 | onMouseDown: PropTypes.func, 56 | onTouchStart: PropTypes.func, 57 | onTouchCancel: PropTypes.func, 58 | onTouchEnd: PropTypes.func 59 | }; 60 | 61 | static defaultProps = { 62 | tag: 'div', 63 | disabled: false, 64 | repeatDelay: 500, 65 | repeatInterval: 32, 66 | repeatCount: 0 67 | }; 68 | 69 | repeatDelayTimer = null; 70 | 71 | repeatIntervalTimer = null; 72 | 73 | repeatAmount = 0; 74 | 75 | acquireTimer = () => { 76 | const repeatDelay = Math.max(Number(this.props.repeatDelay) || 0, 0); 77 | const repeatInterval = Math.max(Number(this.props.repeatInterval) || 0, 0); 78 | const repeatCount = Math.max(Number(this.props.repeatCount) || 0, 0); 79 | 80 | this.repeatAmount = 0; 81 | this.releaseTimer(); 82 | 83 | this.repeatDelayTimer = setTimeout(() => { 84 | if ((repeatCount > 0) && (this.repeatAmount >= repeatCount)) { 85 | return; 86 | } 87 | 88 | this.repeatAmount++; 89 | 90 | if (typeof this.props.onHoldStart === 'function') { 91 | this.props.onHoldStart(); 92 | } 93 | if (typeof this.props.onHold === 'function') { 94 | this.props.onHold(); 95 | } 96 | 97 | this.repeatIntervalTimer = setInterval(() => { 98 | if ((repeatCount > 0) && (this.repeatAmount >= repeatCount)) { 99 | return; 100 | } 101 | 102 | this.repeatAmount++; 103 | 104 | if (typeof this.props.onHold === 'function') { 105 | this.props.onHold(); 106 | } 107 | }, repeatInterval); 108 | }, repeatDelay); 109 | }; 110 | 111 | releaseTimer = () => { 112 | if (this.repeatDelayTimer) { 113 | clearTimeout(this.repeatDelayTimer); 114 | this.repeatDelayTimer = null; 115 | } 116 | if (this.repeatIntervalTimer) { 117 | clearInterval(this.repeatIntervalTimer); 118 | this.repeatIntervalTimer = null; 119 | } 120 | }; 121 | 122 | handleRelease = (event) => { 123 | if (this.props.disabled) { 124 | return; 125 | } 126 | 127 | if (this.repeatAmount > 0) { 128 | if (typeof this.props.onHoldEnd === 'function') { 129 | this.props.onHoldEnd(); 130 | } 131 | } 132 | 133 | this.repeatAmount = 0; 134 | this.releaseTimer(); 135 | 136 | if (typeof this.props.onRelease === 'function') { 137 | this.props.onRelease(event); 138 | } 139 | }; 140 | 141 | handlePress = (event) => { 142 | if (this.props.disabled) { 143 | return; 144 | } 145 | 146 | event.persist(); 147 | 148 | const releaseOnce = (event) => { 149 | document.documentElement.removeEventListener('mouseup', releaseOnce); 150 | this.handleRelease(event); 151 | }; 152 | document.documentElement.addEventListener('mouseup', releaseOnce); 153 | 154 | if (typeof this.props.onPress === 'function') { 155 | this.props.onPress(event); 156 | } 157 | 158 | this.acquireTimer(); 159 | }; 160 | 161 | componentWillUnmount() { 162 | this.repeatAmount = 0; 163 | this.releaseTimer(); 164 | } 165 | 166 | render() { 167 | const { 168 | tag: Tag, 169 | repeatDelay, // eslint-disable-line 170 | repeatInterval, // eslint-disable-line 171 | repeatCount, // eslint-disable-line 172 | onPress, // eslint-disable-line 173 | onHoldStart, // eslint-disable-line 174 | onHold, // eslint-disable-line 175 | onHoldEnd, // eslint-disable-line 176 | onRelease, // eslint-disable-line 177 | onMouseDown, 178 | onTouchStart, 179 | onTouchCancel, 180 | onTouchEnd, 181 | ...props 182 | } = this.props; 183 | 184 | return ( 185 | 205 | ); 206 | } 207 | } 208 | 209 | export default Repeatable; 210 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Repeatable from './Repeatable'; 2 | 3 | export default Repeatable; 4 | -------------------------------------------------------------------------------- /styleguide.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const pkg = require('./package.json'); 4 | 5 | const webpackConfig = { 6 | mode: 'development', 7 | devtool: 'cheap-module-eval-source-map', 8 | devServer: { 9 | disableHostCheck: true, 10 | contentBase: path.resolve(__dirname, 'docs'), 11 | }, 12 | entry: path.resolve(__dirname, 'src/index.js'), 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.jsx?$/, 17 | loader: 'eslint-loader', 18 | enforce: 'pre', 19 | exclude: /node_modules/ 20 | }, 21 | { 22 | test: /\.jsx?$/, 23 | loader: 'babel-loader', 24 | exclude: /node_modules/ 25 | }, 26 | { 27 | test: /\.css$/, 28 | use: [ 29 | 'style-loader', 30 | 'css-loader' 31 | ] 32 | }, 33 | ] 34 | }, 35 | plugins: [ 36 | new webpack.DefinePlugin({ 37 | 'process.env': { 38 | // This has effect on the react lib size 39 | NODE_ENV: JSON.stringify('production') 40 | } 41 | }), 42 | ], 43 | resolve: { 44 | extensions: ['.js', '.json', '.jsx'] 45 | } 46 | }; 47 | 48 | module.exports = { 49 | title: `React Component v${pkg.version}`, 50 | sections: [ 51 | { 52 | name: 'Repeatable', 53 | content: path.resolve(__dirname, 'styleguide/examples/README.md'), 54 | } 55 | ], 56 | require: [ 57 | '@babel/polyfill', 58 | path.resolve(__dirname, 'styleguide/setup.js'), 59 | path.resolve(__dirname, 'styleguide/styles.css'), 60 | ], 61 | ribbon: { 62 | url: pkg.homepage, 63 | text: 'Fork me on GitHub' 64 | }, 65 | serverPort: 8080, 66 | exampleMode: 'collapse', 67 | usageMode: 'expand', 68 | showSidebar: true, 69 | styleguideComponents: { 70 | StyleGuideRenderer: path.join(__dirname, 'styleguide/components/StyleGuideRenderer.jsx'), 71 | Wrapper: path.join(__dirname, 'styleguide/components/Wrapper.jsx'), 72 | }, 73 | styleguideDir: 'docs/', 74 | webpackConfig: webpackConfig 75 | }; 76 | -------------------------------------------------------------------------------- /styleguide/components/Progress.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | const Progress = ({ min = 0, max = 100, now = 0, style, ...props }) => ( 5 |
17 |
31 | {now}% 32 |
33 |
34 | ); 35 | 36 | Progress.propTypes = { 37 | min: PropTypes.number, 38 | max: PropTypes.number, 39 | now: PropTypes.number 40 | }; 41 | 42 | export default Progress; 43 | -------------------------------------------------------------------------------- /styleguide/components/StyleGuideRenderer.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import GitHubCorner from 'react-github-corner'; 4 | import styled from 'styled-components'; 5 | import pkg from '../../package.json'; 6 | 7 | const Root = styled.div` 8 | min-height: 100vh; 9 | background-color: #fff; 10 | padding-left: 240px; 11 | `; 12 | 13 | const Main = styled.main` 14 | padding: 16px 32px; 15 | margin: 0 auto; 16 | display: block; 17 | `; 18 | 19 | const Sidebar = styled.div` 20 | background-color: #f5f5f5; 21 | border: #e8e8e8 solid; 22 | border-width: 0 1px 0 0; 23 | position: fixed; 24 | top: 0; 25 | left: 0; 26 | bottom: 0; 27 | width: 240px; 28 | overflow: auto; 29 | -webkit-overflow-scrolling: touch; 30 | `; 31 | 32 | const Block = styled.div``; 33 | 34 | const TextBlock = styled(Block)` 35 | padding: 16px; 36 | border-bottom: 1px #e8e8e8 solid; 37 | color: #333; 38 | margin: 0; 39 | font-size: 18px; 40 | font-weight: normal; 41 | `; 42 | 43 | const StyleGuideRenderer = ({ 44 | title, 45 | toc, 46 | children, 47 | }) => { 48 | return ( 49 | 50 | 51 | 52 | 53 | {title} 54 | 55 | 56 | {toc} 57 | 58 | 59 |
60 | {children} 61 |
62 |
63 | ); 64 | }; 65 | 66 | StyleGuideRenderer.propTypes = { 67 | title: PropTypes.string, 68 | toc: PropTypes.node, 69 | }; 70 | 71 | export default StyleGuideRenderer; 72 | -------------------------------------------------------------------------------- /styleguide/components/Wrapper.jsx: -------------------------------------------------------------------------------- 1 | const Wrapper = ({ children }) => { 2 | return children; 3 | }; 4 | 5 | export default Wrapper; 6 | -------------------------------------------------------------------------------- /styleguide/examples/README.md: -------------------------------------------------------------------------------- 1 | ```jsx 2 | import 'react-bootstrap-buttons/dist/react-bootstrap-buttons.css'; 3 | import 'rc-slider/dist/rc-slider.css'; 4 | import cx from 'classnames'; 5 | import { Button } from 'react-bootstrap-buttons'; 6 | import Slider from 'rc-slider'; 7 | import Progress from '../components/Progress'; 8 | 9 | initialState = { 10 | repeatDelay: Repeatable.defaultProps.repeatDelay, 11 | repeatInterval: Repeatable.defaultProps.repeatInterval, 12 | repeatCount: 0, 13 | button1: { 14 | pressed: false, 15 | holding: false 16 | }, 17 | button2: { 18 | pressed: false, 19 | holding: false 20 | }, 21 | value: 0 22 | }; 23 | 24 | const RepeatableButton = ({ onClick, ...props }) => ( 25 | 31 | ); 32 | 33 |
34 |
35 | 38 |
39 | { 45 | setState({ repeatDelay: value }); 46 | }} 47 | /> 48 |
49 |
50 |
51 | 54 |
55 | { 61 | setState({ repeatInterval: value }); 62 | }} 63 | /> 64 |
65 |
66 |
67 | 70 |
71 | { 77 | setState({ repeatCount: value }); 78 | }} 79 | /> 80 |
81 |
82 |
83 | 89 |
90 | { 101 | console.log('[1] onPress'); 102 | setState({ 103 | button1: { 104 | pressed: true, 105 | holding: false 106 | } 107 | }); 108 | }} 109 | onHoldStart={() => { 110 | console.log('[1] onHoldStart'); 111 | setState({ 112 | value: 0 113 | }); 114 | }} 115 | onHold={() => { 116 | console.log('[1] onHold'); 117 | setState(state => ({ 118 | button1: { 119 | pressed: true, 120 | holding: true 121 | }, 122 | value: Math.min(state.value + 1, 100) 123 | })); 124 | }} 125 | onHoldEnd={() => { 126 | console.log('[1] onHoldEnd'); 127 | setState({ 128 | value: 0 129 | }); 130 | }} 131 | onRelease={() => { 132 | console.log('[1] onRelease'); 133 | setState({ 134 | button1: { 135 | pressed: false, 136 | holding: false 137 | } 138 | }); 139 | }} 140 | > 141 | {!state.button1.pressed && 'Press Me (1x)'} 142 | {state.button1.pressed && !state.button1.holding && 'Pressing... (1x)'} 143 | {state.button1.pressed && state.button1.holding && 'Holding (1x)'} 144 | 145 | { 156 | console.log('[2] onPress'); 157 | setState({ 158 | button2: { 159 | pressed: true, 160 | holding: false 161 | } 162 | }); 163 | }} 164 | onHoldStart={() => { 165 | console.log('[2] onHoldStart'); 166 | setState({ 167 | value: 0 168 | }); 169 | }} 170 | onHold={() => { 171 | console.log('[2] onHold'); 172 | setState(state => ({ 173 | button2: { 174 | pressed: true, 175 | holding: true 176 | }, 177 | value: Math.min(state.value + 1, 100) 178 | })); 179 | }} 180 | onHoldEnd={() => { 181 | console.log('[2] onHoldEnd'); 182 | setState({ 183 | value: 0 184 | }); 185 | }} 186 | onRelease={(event) => { 187 | console.log('[2] onRelease'); 188 | setState({ 189 | button2: { 190 | pressed: false, 191 | holding: false 192 | } 193 | }); 194 | }} 195 | > 196 | {!state.button2.pressed && 'Press Me (5x)'} 197 | {state.button2.pressed && !state.button2.holding && 'Pressing... (5x)'} 198 | {state.button2.pressed && state.button2.holding && 'Holding (5x)'} 199 | 200 |
201 | ``` 202 | -------------------------------------------------------------------------------- /styleguide/setup.js: -------------------------------------------------------------------------------- 1 | import Repeatable from '../src/Repeatable'; 2 | 3 | global.Repeatable = Repeatable; 4 | -------------------------------------------------------------------------------- /styleguide/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; 3 | } 4 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import sinon from 'sinon'; 4 | import { test } from 'tap'; 5 | import '../setupTests'; 6 | import Repeatable from '../src'; 7 | 8 | test('', (t) => { 9 | const wrapper = mount(( 10 | 11 | 12 | 13 | )); 14 | t.equal(wrapper.find(Repeatable).length, 1, 'should render component'); 15 | t.end(); 16 | }); 17 | 18 | test('simulates click event', (t) => { 19 | const onClick = sinon.spy(); 20 | const wrapper = mount(); 21 | wrapper.find(Repeatable).simulate('click'); 22 | t.ok(onClick.calledOnce, 'should be called once'); 23 | t.end(); 24 | }); 25 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const findImports = require('find-imports'); 3 | const webpack = require('webpack'); 4 | const pkg = require('./package.json'); 5 | const babelConfig = require('./babel.config'); 6 | 7 | module.exports = { 8 | mode: 'development', 9 | devtool: 'source-map', 10 | entry: { 11 | [pkg.name]: path.resolve(__dirname, 'src/index.js') 12 | }, 13 | output: { 14 | path: path.join(__dirname, 'lib'), 15 | filename: 'index.js', 16 | libraryTarget: 'commonjs2' 17 | }, 18 | externals: [] 19 | .concat(findImports(['src/**/*.{js,jsx}'], { flatten: true })) 20 | .concat(Object.keys(pkg.peerDependencies)) 21 | .concat(Object.keys(pkg.dependencies)), 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.jsx?$/, 26 | loader: 'eslint-loader', 27 | enforce: 'pre', 28 | exclude: /node_modules/ 29 | }, 30 | { 31 | test: /\.jsx?$/, 32 | loader: 'babel-loader', 33 | exclude: /node_modules/, 34 | options: babelConfig 35 | }, 36 | ] 37 | }, 38 | plugins: [ 39 | new webpack.DefinePlugin({ 40 | 'process.env': { 41 | // This has effect on the react lib size 42 | NODE_ENV: JSON.stringify('production') 43 | } 44 | }), 45 | ], 46 | resolve: { 47 | extensions: ['.js', '.json', '.jsx'] 48 | } 49 | }; 50 | --------------------------------------------------------------------------------