├── .gitignore ├── .travis.yml ├── HISTORY.md ├── README.md ├── assets └── index.less ├── browserslist ├── examples ├── left.html ├── left.native.tsx ├── left.tsx ├── mutiple.html ├── mutiple.tsx ├── right.html ├── right.native.tsx ├── right.tsx ├── simple.html ├── simple.native.tsx └── simple.tsx ├── index.android.js ├── index.ios.js ├── index.js ├── package.json ├── src ├── PropTypes.tsx ├── Swipeout.native.tsx ├── Swipeout.tsx ├── index.native.tsx └── index.tsx ├── tests ├── index.js └── usage.tsx ├── tsconfig.json └── typings ├── custom.d.ts └── index.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea/ 3 | .ipr 4 | .iws 5 | *~ 6 | ~* 7 | *.diff 8 | *.patch 9 | *.bak 10 | .DS_Store 11 | Thumbs.db 12 | .project 13 | .*proj 14 | .svn/ 15 | *.swp 16 | *.swo 17 | *.pyc 18 | *.pyo 19 | *.log 20 | *.log.* 21 | node_modules 22 | dist 23 | build 24 | lib 25 | coverage 26 | _ts2js 27 | es 28 | ios 29 | android 30 | yarn.lock 31 | assets/index.css 32 | package-lock.json 33 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | notifications: 4 | email: 5 | - rjmuqiang@gmail.com 6 | - hust2012jiangkai@gmail.com 7 | - quentinyang1985@gmail.com 8 | 9 | node_js: 10 | - 6.9.4 11 | 12 | before_install: 13 | - | 14 | if ! git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '(\.md$)|(^(docs|examples))/' 15 | then 16 | echo "Only docs were updated, stopping build process." 17 | exit 18 | fi 19 | phantomjs --version 20 | script: 21 | - | 22 | if [ "$TEST_TYPE" = test ]; then 23 | npm test 24 | else 25 | npm run $TEST_TYPE 26 | fi 27 | env: 28 | matrix: 29 | - TEST_TYPE=lint 30 | - TEST_TYPE=test 31 | - TEST_TYPE=coverage 32 | 33 | matrix: 34 | allow_failures: 35 | - env: "TEST_TYPE=saucelabs" 36 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | # 2.0.7 4 | - add browserslist config file, then tell babel need to add prefix for `display: flex`. 5 | - refine swipe and demo 6 | - fix: https://github.com/ant-design/ant-design-mobile/issues/1954 (#56) 7 | 8 | # 2.0.6 9 | 10 | - fix: https://github.com/ant-design/ant-design-mobile/issues/1954 As rc-gesture revert preventDefault in v0.0.19 for tabs+listview issue (https://github.com/ant-design/ant-design-mobile/issues/2589). 11 | - Then, rc-gesture@v0.0.20 fixed this issue by exposing the `event` as a property of Gesture object, and the Gesture object will be passed as the first parameter when invoked panMove event callback. 12 | - So, `rc-swipeout` invokes `event.preventDefault()` to prevent scroll event when pan moving. 13 | 14 | # 2.0.0 15 | 16 | - replace `hammer.js` width [rc-gesture](https://github.com/react-component/gesture) 17 | 18 | # 1.4.5 19 | 20 | - replace `object.omit`, update style 21 | 22 | # ~1.4.4 23 | 24 | - improve: disabled swipe if deltaX < deltaY 25 | 26 | # 1.4.0 27 | 28 | - improve: auto width for swipe buttons, #40 29 | 30 | # 1.3.8 31 | 32 | - fixed: only one hand can be swiped if left or right is null. 33 | 34 | # ~1.3.7 35 | 36 | - fixed `removeEventListener` bug; 37 | - fixed binding and unbinding event bug; 38 | - support es module; 39 | - update deps; 40 | 41 | # 1.3.1 42 | 43 | - update react-native-swipeout version 44 | 45 | # 1.2.7 46 | 47 | - new: role="button" 48 | 49 | # 1.2.6 50 | 51 | - new: support `className` for button; 52 | 53 | # 1.2.5 54 | 55 | - fix: onCloseSwipe prefixCls bug; 56 | 57 | ## 1.2.3 58 | 59 | - add cover `div`, for body touchstart 60 | - fix: buttons cannot be hidden when pan short distance 61 | 62 | ## 1.2.2 63 | 64 | - prevent default of event; 65 | 66 | ## 1.2.1 67 | 68 | - support `onClose` for rn; 69 | 70 | ## 1.2.0 71 | 72 | - click body to close swipe buttons; #19, #10 73 | - support event arg for onPress; #18 74 | - swipe to close for web; 75 | 76 | ## 1.1.5 77 | 78 | - fix issue/9 79 | 80 | ## 1.1.4 81 | 82 | - use babel-runtime 83 | 84 | ## 1.1.3 85 | 86 | - update deps 87 | 88 | ## 1.1.0 89 | 90 | - [`new`] react-native support 91 | 92 | ## 1.0.2 93 | 94 | - [`fix`] error if this.refs.left/right is [] 95 | 96 | ## 1.0.1 97 | - [`fix`] npm package empty 98 | 99 | ## 1.0.0 100 | init project 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rc-swipeout 2 | --- 3 | 4 | iOS-style swipeout buttons that appear from behind a component (web & react-native support) 5 | 6 | [![NPM version][npm-image]][npm-url] 7 | [![build status][travis-image]][travis-url] 8 | [![Test coverage][coveralls-image]][coveralls-url] 9 | 10 | [npm-image]: http://img.shields.io/npm/v/rc-swipeout.svg?style=flat-square 11 | [npm-url]: http://npmjs.org/package/rc-swipeout 12 | [travis-image]: https://img.shields.io/travis/react-component/swipeout.svg?style=flat-square 13 | [travis-url]: https://travis-ci.org/react-component/swipeout 14 | [coveralls-image]: https://img.shields.io/coveralls/react-component/swipeout.svg?style=flat-square 15 | [coveralls-url]: https://coveralls.io/r/react-component/swipeout?branch=master 16 | 17 | ## Screenshots 18 | 19 | ![rc-swipeout](https://zos.alipayobjects.com/rmsportal/dqxQTtxrKrGMVEc.gif) 20 | 21 | ## Installation 22 | 23 | `npm install --save rc-swipeout` 24 | 25 | ## Development 26 | 27 | ``` 28 | web: 29 | npm install 30 | npm start 31 | 32 | rn: 33 | tnpm run rn-start 34 | ``` 35 | 36 | ## Example 37 | 38 | - local: http://localhost:8000/examples/ 39 | - online: http://react-component.github.io/swipeout/ 40 | 41 | ## react-native 42 | 43 | ``` 44 | ./node_modules/rc-tools run react-native-init 45 | react-native run-ios 46 | ``` 47 | 48 | ## Usage 49 | 50 | ```js 51 | import Swipeout from 'rc-swipeout'; 52 | import 'rc-swipeout/assets/index.less'; (web only) 53 | 54 | console.log('reply'), 59 | style: { backgroundColor: 'orange', color: 'white' }, 60 | className: 'custom-class-1' 61 | } 62 | ]} 63 | right={[ 64 | { 65 | text: 'delete', 66 | onPress:() => console.log('delete'), 67 | style: { backgroundColor: 'red', color: 'white' }, 68 | className: 'custom-class-2' 69 | } 70 | ]} 71 | onOpen={() => console.log('open')} 72 | onClose={() => console.log('close')} 73 | > 74 |
swipeout demo
75 |
76 | 77 | ``` 78 | 79 | 80 | ## API 81 | 82 | ### props 83 | 84 | | name | description | type | default | 85 | |-------------|------------------------|--------|------------| 86 | | prefixCls | className prefix | String | `rc-swipeout` | 87 | | style | swipeout style | Object | `` | 88 | | left | swipeout buttons on left | Array | `[]` | 89 | | right | swipeout buttons on right | Array | `[]` | 90 | | autoClose | auto close on button press | Boolean | `function() {}` | 91 | | onOpen | | Function | `function() {}` | 92 | | onClose | | Function | `function() {}` | 93 | | disabled | disabled swipeout | Boolean | `false` | 94 | 95 | ### button props 96 | 97 | | name | description | type | default | 98 | |-------------|------------------------|--------|------------| 99 | | text | button text | String | `Click` | 100 | | style | button style | Object | `` | 101 | | onPress | button press function | Function | `function() {}` | 102 | | className | button custom class | String | `` | 103 | 104 | ## Test Case 105 | 106 | ``` 107 | npm test 108 | npm run chrome-test 109 | ``` 110 | 111 | ## Coverage 112 | 113 | ``` 114 | npm run coverage 115 | ``` 116 | 117 | open coverage/ dir 118 | 119 | ## License 120 | 121 | rc-swipeout is released under the MIT license. 122 | -------------------------------------------------------------------------------- /assets/index.less: -------------------------------------------------------------------------------- 1 | @swipeout-prefix-cls: rc-swipeout; 2 | 3 | .@{swipeout-prefix-cls} { 4 | overflow: hidden; 5 | position: relative; 6 | &-content { 7 | position: relative; 8 | background-color: #fff; 9 | } 10 | &-cover { 11 | position: absolute; 12 | z-index: 2; 13 | background: transparent; 14 | height: 100%; 15 | width: 100%; 16 | top: 0; 17 | display: none; 18 | } 19 | & .@{swipeout-prefix-cls}-content, 20 | & .@{swipeout-prefix-cls}-actions { 21 | transition: all 250ms; 22 | } 23 | &-swiping .@{swipeout-prefix-cls}-content { 24 | transition: none; 25 | } 26 | &-actions { 27 | position: absolute; 28 | top: 0; 29 | bottom: 0; 30 | display: flex; 31 | overflow: hidden; 32 | white-space: nowrap; 33 | &-left { 34 | left: 0; 35 | } 36 | &-right { 37 | right: 0; 38 | } 39 | } 40 | &-btn { 41 | display: flex; 42 | align-items: center; 43 | justify-content: center; 44 | position: relative; 45 | overflow: hidden; 46 | &-text { 47 | padding: 0 12px; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | # Browsers that we support 2 | last 2 versions 3 | Firefox ESR 4 | > 2% 5 | ie >= 8 6 | iOS >= 8 7 | Android >= 4 8 | -------------------------------------------------------------------------------- /examples/left.html: -------------------------------------------------------------------------------- 1 | placeholder -------------------------------------------------------------------------------- /examples/left.native.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* tslint:disable:no-console */ 3 | import { View, Text } from 'react-native'; 4 | import Swipeout from '../src/'; 5 | import React from 'react'; 6 | 7 | const SwipeoutExample = () => ( 8 | 9 | console.log('read'), 16 | style: { backgroundColor: 'blue', color: 'white' }, 17 | }, 18 | { 19 | text: 'reply', 20 | onPress: () => console.log('reply'), 21 | style: { backgroundColor: 'green', color: 'white' }, 22 | }, 23 | ]} 24 | onOpen={() => console.log('open')} 25 | onClose={() => console.log('close')} 26 | > 27 | this is Demo 33 | 34 | 35 | ); 36 | 37 | export const Demo = SwipeoutExample; 38 | export const title = 'Left'; 39 | -------------------------------------------------------------------------------- /examples/left.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* tslint:disable:no-console */ 3 | import '../assets/index.less'; 4 | import Swipeout from '../src/'; 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | 8 | ReactDOM.render( 9 |
10 | console.log('read'), 17 | style: { backgroundColor: 'blue', color: 'white' }, 18 | }, 19 | { 20 | text: 'reply', 21 | onPress: () => console.log('reply'), 22 | style: { backgroundColor: 'green', color: 'white' }, 23 | }, 24 | ]} 25 | onOpen={() => console.log('open')} 26 | onClose={() => console.log('close')} 27 | > 28 |
swipe out simple demo
36 |
37 |
, 38 | document.getElementById('__react-content'), 39 | ); 40 | -------------------------------------------------------------------------------- /examples/mutiple.html: -------------------------------------------------------------------------------- 1 | placeholder 2 | -------------------------------------------------------------------------------- /examples/mutiple.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* tslint:disable:no-console */ 3 | import '../assets/index.less'; 4 | import Swipeout from '../src/'; 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | 8 | class SwipeoutExample extends React.Component { 9 | state = { 10 | items: ['00', '01', '02', '03', '04', '05'], 11 | }; 12 | onDelete(value) { 13 | const tempArr = this.state.items; 14 | this.setState({ 15 | items: tempArr.filter(v => v !== value), 16 | }); 17 | } 18 | render() { 19 | return ( 20 |
21 |

多个实例:

22 | {this.state.items.map((item, i) => this.onDelete(item), 29 | style: { backgroundColor: '#F4333C', color: 'white', width: 80 }, 30 | }, 31 | ]} 32 | > 33 |
{ 34 | console.log(`pressed item ${item}`); 35 | }} 36 | > 37 | item {item} 38 |
39 |
, 40 | )} 41 |
42 | ); 43 | } 44 | } 45 | 46 | ReactDOM.render(, 47 | document.getElementById('__react-content'), 48 | ); 49 | -------------------------------------------------------------------------------- /examples/right.html: -------------------------------------------------------------------------------- 1 | placeholder -------------------------------------------------------------------------------- /examples/right.native.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* tslint:disable:no-console */ 3 | import { View, Text } from 'react-native'; 4 | import Swipeout from '../src/'; 5 | import React from 'react'; 6 | 7 | const SwipeoutExample = () => ( 8 | 9 | console.log('more'), 15 | style: { backgroundColor: 'orange', color: 'white' }, 16 | }, 17 | { text: 'delete', 18 | onPress: () => console.log('delete'), 19 | style: { backgroundColor: 'red', color: 'white' }, 20 | }, 21 | ]} 22 | onOpen={() => console.log('open')} 23 | onClose={() => console.log('close')} 24 | > 25 | this is Demo 31 | 32 | 33 | ); 34 | 35 | export const Demo = SwipeoutExample; 36 | export const title = 'Right'; 37 | -------------------------------------------------------------------------------- /examples/right.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* tslint:disable:no-console */ 3 | import '../assets/index.less'; 4 | import Swipeout from '../src/'; 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | 8 | ReactDOM.render( 9 |
10 | console.log('more'), 16 | style: { backgroundColor: 'orange', color: 'white' }, 17 | }, 18 | { text: 'delete', 19 | onPress: () => console.log('delete'), 20 | style: { backgroundColor: 'red', color: 'white' }, 21 | }, 22 | ]} 23 | onOpen={() => console.log('open')} 24 | onClose={() => console.log('close')} 25 | > 26 |
{ 27 | console.log('emit an event on children element!'); 28 | }} style={{ 29 | height: 44, 30 | backgroundColor: 'white', 31 | lineHeight: '44px', 32 | borderTop: '1px solid #dedede', 33 | borderBottom: '1px solid #dedede', 34 | }} 35 | >swipe out simple demo
36 |
37 |
, 38 | document.getElementById('__react-content'), 39 | ); 40 | -------------------------------------------------------------------------------- /examples/simple.html: -------------------------------------------------------------------------------- 1 | placeholder -------------------------------------------------------------------------------- /examples/simple.native.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* tslint:disable:no-console */ 3 | import { View, Text } from 'react-native'; 4 | import Swipeout from '../src/index.native'; 5 | import React from 'react'; 6 | 7 | const SwipeoutExample = () => ( 8 | 9 | more, 15 | onPress: () => console.log('more'), 16 | style: { backgroundColor: 'orange', color: 'white' }, 17 | }, 18 | { text: 'delete', 19 | onPress: () => console.log('delete'), 20 | style: { backgroundColor: 'red', color: 'white' }, 21 | }, 22 | ]} 23 | left={[ 24 | { 25 | text: 'read', 26 | onPress: () => console.log('read'), 27 | style: { backgroundColor: 'blue', color: 'white' }, 28 | }, 29 | { 30 | text: 'reply', 31 | onPress: () => console.log('reply'), 32 | style: { backgroundColor: 'green', color: 'white' }, 33 | }, 34 | ]} 35 | onOpen={() => console.log('open')} 36 | onClose={() => console.log('close')} 37 | > 38 | { 45 | console.log('onPress children'); 46 | }} 47 | >this is Demo 48 | 49 | 50 | ); 51 | 52 | export const Demo = SwipeoutExample; 53 | export const title = 'Simple'; 54 | -------------------------------------------------------------------------------- /examples/simple.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* tslint:disable:no-console */ 3 | import 'rc-swipeout/assets/index.less'; 4 | import Swipeout from '../src/'; 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | 8 | const SwipeDemo = () => ( 9 | more more, 15 | onPress: () => console.log('more more'), 16 | style: { backgroundColor: 'orange', color: 'white' }, 17 | }, 18 | { text: 'delete', 19 | onPress: () => console.log('delete'), 20 | style: { backgroundColor: 'red', color: 'white' }, 21 | }, 22 | ]} 23 | left={[ 24 | { 25 | text: 'read', 26 | onPress: () => console.log('read'), 27 | style: { backgroundColor: 'blue', color: 'white' }, 28 | }, 29 | { 30 | text: 'reply me', 31 | onPress: () => console.log('reply me'), 32 | style: { backgroundColor: 'green', color: 'white' }, 33 | }, 34 | { 35 | text: 'reply other people', 36 | onPress: () => console.log('reply other people'), 37 | style: { backgroundColor: 'gray', color: 'white' }, 38 | }, 39 | ]} 40 | onOpen={() => console.log('open')} 41 | onClose={() => console.log('close')} 42 | > 43 |
swipe out simple demo 51 |
52 |
53 | ); 54 | 55 | ReactDOM.render( 56 |
57 |
Basic
58 | 59 |
60 |
Test scroll
61 |
62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 |
73 |
, 74 | document.getElementById('__react-content'), 75 | ); 76 | -------------------------------------------------------------------------------- /index.android.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./index.ios'); 2 | -------------------------------------------------------------------------------- /index.ios.js: -------------------------------------------------------------------------------- 1 | import getList from 'react-native-index-page'; 2 | 3 | getList({ 4 | demos: [ 5 | // require('./_ts2js/examples/left'), 6 | // require('./_ts2js/examples/right'), 7 | require('./_ts2js/examples/simple'), 8 | ], 9 | title: require('./package.json').name, 10 | }); 11 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | import Swipeout from './src/'; 3 | export default Swipeout; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rc-swipeout", 3 | "version": "2.0.11", 4 | "description": "swipe out ui component for react(web and react-native)", 5 | "keywords": [ 6 | "react", 7 | "react-component", 8 | "swipeout", 9 | "rc-swipeout", 10 | "swipe delete" 11 | ], 12 | "homepage": "https://github.com/react-component/swipeout", 13 | "author": "rjmuqiang@gmail.com", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/react-component/swipeout.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/react-component/swipeout/issues" 20 | }, 21 | "files": [ 22 | "lib", 23 | "es", 24 | "dist", 25 | "assets/*.css" 26 | ], 27 | "licenses": "MIT", 28 | "main": "./lib/index", 29 | "module": "./es/index", 30 | "config": { 31 | "port": 8000, 32 | "entry": { 33 | "rc-swipeout": [ 34 | "./index.js", 35 | "./assets/index.less" 36 | ] 37 | } 38 | }, 39 | "scripts": { 40 | "dist": "rc-tools run dist", 41 | "watch-tsc": "rc-tools run watch-tsc", 42 | "compile": "rc-tools run compile --babel-runtime", 43 | "build": "rc-tools run build", 44 | "gh-pages": "rc-tools run gh-pages", 45 | "start": "rc-tools run server", 46 | "pub": "rc-tools run pub --babel-runtime", 47 | "lint": "rc-tools run lint", 48 | "karma": "rc-test run karma", 49 | "test": "rc-test run test", 50 | "chrome-test": "rc-test run chrome-test", 51 | "coverage": "rc-test run coverage", 52 | "rn-start": "node node_modules/react-native/local-cli/cli.js start", 53 | "prepublish": "rc-tools run guard", 54 | "prepare": "rc-tools run guard", 55 | "prepublishOnly": "rc-tools run guard" 56 | }, 57 | "dependencies": { 58 | "babel-runtime": "6.x", 59 | "classnames": "2.x", 60 | "rc-gesture": "~0.0.22", 61 | "react-native-swipeout": "^2.2.2" 62 | }, 63 | "devDependencies": { 64 | "@types/mocha": "~2.2.32", 65 | "@types/react": "latest", 66 | "@types/react-dom": "latest", 67 | "@types/react-native": "latest", 68 | "expect.js": "0.3.x", 69 | "pre-commit": "1.x", 70 | "rc-test": "~6.0.3", 71 | "rc-tools": "6.x", 72 | "react": "^16.0.0", 73 | "react-dom": "^16.0.0", 74 | "react-native": "~0.55.4", 75 | "react-native-index-page": "~0.2.0", 76 | "react-navigation": "1.0.0-beta.12" 77 | }, 78 | "typings": "./lib/index.d.ts", 79 | "pre-commit": [ 80 | "lint" 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /src/PropTypes.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface IPropTypes { 4 | left?: Array<{ text: React.ReactNode; onPress?: () => void; type?: any; style?: any; className?: string}>; 5 | right?: Array<{ text: React.ReactNode; onPress?: () => void; type?: any; style?: any; className?: string}>; 6 | autoClose?: boolean; 7 | onOpen?: () => void; 8 | onClose?: () => void; 9 | disabled?: boolean; 10 | style?: any; 11 | /* web only */ 12 | prefixCls?: string; 13 | } 14 | 15 | export default IPropTypes; 16 | -------------------------------------------------------------------------------- /src/Swipeout.native.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Text } from 'react-native'; 3 | import Swipe from 'react-native-swipeout'; 4 | import SwipeoutPropType from './PropTypes'; 5 | 6 | export type SwipeButttonType = { 7 | backgroundColor?: string; 8 | color?: string; 9 | component?: JSX.Element; 10 | text?: string; 11 | type?: 'default' | 'delete' | 'primary' | 'secondary'; 12 | underlayColor?: string; 13 | disabled?: boolean; 14 | onPress?(): void; 15 | }; 16 | 17 | class Swipeout extends React.Component { 18 | static defaultProps = { 19 | autoClose: false, 20 | disabled: false, 21 | onOpen() {}, 22 | onClose() {}, 23 | }; 24 | 25 | constructor(props) { 26 | super(props); 27 | this.state = { 28 | show: false, 29 | paddingTop: 0, 30 | }; 31 | } 32 | 33 | renderCustomButton(button) { 34 | const buttonStyle = button.style || {}; 35 | const bgColor = buttonStyle.backgroundColor || 'transparent'; 36 | const Component = ( 37 | 42 | {React.isValidElement(button.text) ? button.text : ( 43 | 44 | {button.text} 45 | 46 | )} 47 | 48 | ); 49 | return { 50 | text: button.text || 'Click', 51 | onPress: button.onPress, 52 | type: 'default', 53 | component: Component, 54 | backgroundColor: 'transparent', 55 | color: '#999', 56 | disabled: false, 57 | }; 58 | } 59 | 60 | render() { 61 | const { disabled, autoClose, style, left, right, onOpen, onClose, children, ...restProps } = this.props; 62 | 63 | const customLeft = left && left.map(btn => { 64 | return this.renderCustomButton(btn); 65 | }); 66 | const customRight = right && right.map(btn => { 67 | return this.renderCustomButton(btn); 68 | }); 69 | 70 | return customLeft || customRight ? ( 71 | 80 | {children} 81 | 82 | ) : ( 83 | 84 | {children} 85 | 86 | ); 87 | } 88 | } 89 | 90 | export default Swipeout; 91 | -------------------------------------------------------------------------------- /src/Swipeout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Gesture from 'rc-gesture'; 4 | import classnames from 'classnames'; 5 | import SwipeoutPropType from './PropTypes'; 6 | 7 | // https://developer.mozilla.org/en-US/docs/Web/API/Element/matches 8 | // http://caniuse.com/#search=match 9 | function closest(el, selector) { 10 | const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; 11 | 12 | while (el) { 13 | if (matchesSelector.call(el, selector)) { 14 | return el; 15 | } else { 16 | el = el.parentElement; 17 | } 18 | } 19 | return null; 20 | } 21 | 22 | export default class Swipeout extends React.Component { 23 | static defaultProps = { 24 | prefixCls: 'rc-swipeout', 25 | autoClose: false, 26 | disabled: false, 27 | left: [], 28 | right: [], 29 | onOpen() {}, 30 | onClose() {}, 31 | }; 32 | 33 | openedLeft: boolean; 34 | openedRight: boolean; 35 | content: any; 36 | cover: any; 37 | left: any; 38 | right: any; 39 | btnsLeftWidth: number; 40 | btnsRightWidth: number; 41 | swiping: boolean; 42 | needShowLeft: boolean; 43 | needShowRight: boolean; 44 | 45 | constructor(props) { 46 | super(props); 47 | this.state = { 48 | swiping: false, 49 | }; 50 | this.openedLeft = false; 51 | this.openedRight = false; 52 | } 53 | 54 | componentDidMount() { 55 | this.btnsLeftWidth = this.left ? this.left.offsetWidth : 0; 56 | this.btnsRightWidth = this.right ? this.right.offsetWidth : 0; 57 | document.body.addEventListener('touchstart', this.onCloseSwipe, true); 58 | } 59 | 60 | componentWillUnmount() { 61 | document.body.removeEventListener('touchstart', this.onCloseSwipe, true); 62 | } 63 | 64 | onCloseSwipe = (ev) => { 65 | if (!(this.openedLeft || this.openedRight)) { 66 | return; 67 | } 68 | const pNode = closest(ev.target, `.${this.props.prefixCls}-actions`); 69 | if (!pNode) { 70 | ev.preventDefault(); 71 | this.close(); 72 | } 73 | } 74 | 75 | onPanStart = (e) => { 76 | const { direction, moveStatus } = e; 77 | const { x: deltaX } = moveStatus; 78 | // http://hammerjs.github.io/api/#directions 79 | const isLeft = direction === 2; 80 | const isRight = direction === 4; 81 | 82 | if (!isLeft && !isRight) { 83 | return; 84 | } 85 | const { left, right } = this.props; 86 | this.needShowRight = isLeft && right!.length > 0; 87 | this.needShowLeft = isRight && left!.length > 0; 88 | if (this.left) { 89 | this.left.style.visibility = this.needShowRight ? 'hidden' : 'visible'; 90 | } 91 | if (this.right) { 92 | this.right.style.visibility = this.needShowLeft ? 'hidden' : 'visible'; 93 | } 94 | if (this.needShowLeft || this.needShowRight) { 95 | this.swiping = true; 96 | this.setState({ 97 | swiping: this.swiping, 98 | }); 99 | this._setStyle(deltaX); 100 | } 101 | } 102 | onPanMove = (e) => { 103 | const { moveStatus, srcEvent } = e; 104 | const { x: deltaX } = moveStatus; 105 | if (!this.swiping) { 106 | return; 107 | } 108 | 109 | // fixed scroll when it's pan and moving. 110 | if (srcEvent && srcEvent.preventDefault) { 111 | srcEvent.preventDefault(); 112 | } 113 | this._setStyle(deltaX); 114 | } 115 | 116 | onPanEnd = (e) => { 117 | if (!this.swiping) { 118 | return; 119 | } 120 | 121 | const { moveStatus } = e; 122 | const { x: deltaX } = moveStatus; 123 | 124 | const needOpenRight = this.needShowRight && Math.abs(deltaX) > this.btnsRightWidth / 2; 125 | const needOpenLeft = this.needShowLeft && Math.abs(deltaX) > this.btnsLeftWidth / 2; 126 | 127 | if (needOpenRight) { 128 | this.doOpenRight(); 129 | } else if (needOpenLeft) { 130 | this.doOpenLeft(); 131 | } else { 132 | this.close(); 133 | } 134 | this.swiping = false; 135 | this.setState({ 136 | swiping: this.swiping, 137 | }); 138 | this.needShowLeft = false; 139 | this.needShowRight = false; 140 | } 141 | 142 | doOpenLeft = () => { 143 | this.open(this.btnsLeftWidth, true, false); 144 | } 145 | 146 | doOpenRight = () => { 147 | this.open(-this.btnsRightWidth, true, false); 148 | } 149 | // left & right button click 150 | onBtnClick(ev, btn) { 151 | const onPress = btn.onPress; 152 | if (onPress) { 153 | onPress(ev); 154 | } 155 | if (this.props.autoClose) { 156 | this.close(); 157 | } 158 | } 159 | 160 | _getContentEasing(value, limit) { 161 | // limit content style left when value > actions width 162 | const delta = Math.abs(value) - Math.abs(limit); 163 | const isOverflow = delta > 0; 164 | const factor = limit > 0 ? 1 : -1; 165 | if (isOverflow) { 166 | value = limit + Math.pow(delta, 0.85) * factor; 167 | return Math.abs(value) > Math.abs(limit) ? limit : value; 168 | } 169 | return value; 170 | } 171 | 172 | // set content & actions style 173 | _setStyle = (value) => { 174 | const limit = value > 0 ? this.btnsLeftWidth : -this.btnsRightWidth; 175 | const contentLeft = this._getContentEasing(value, limit); 176 | this.content.style.left = `${contentLeft}px`; 177 | if (this.cover) { 178 | this.cover.style.display = Math.abs(value) > 0 ? 'block' : 'none'; 179 | this.cover.style.left = `${contentLeft}px`; 180 | } 181 | } 182 | 183 | open = (value, openedLeft, openedRight) => { 184 | if (!this.openedLeft && !this.openedRight && this.props.onOpen) { 185 | this.props.onOpen(); 186 | } 187 | 188 | this.openedLeft = openedLeft; 189 | this.openedRight = openedRight; 190 | this._setStyle(value); 191 | } 192 | 193 | close = () => { 194 | if ((this.openedLeft || this.openedRight) && this.props.onClose) { 195 | this.props.onClose(); 196 | } 197 | this._setStyle(0); 198 | this.openedLeft = false; 199 | this.openedRight = false; 200 | } 201 | 202 | renderButtons(buttons, ref) { 203 | const prefixCls = this.props.prefixCls; 204 | 205 | return (buttons && buttons.length) ? ( 206 |
this[ref] = el} 209 | > 210 | { 211 | buttons.map((btn, i) => ( 212 |
this.onBtnClick(e, btn)} 217 | > 218 |
{btn.text || 'Click'}
219 |
220 | )) 221 | } 222 |
223 | ) : null; 224 | } 225 | 226 | onTouchMove = (e) => { 227 | if (this.swiping) { 228 | e.preventDefault(); 229 | } 230 | } 231 | 232 | render() { 233 | const { prefixCls, left, right, disabled, children, ...restProps } = this.props; 234 | 235 | const { autoClose, onOpen, onClose, ...divProps } = restProps; 236 | 237 | const cls = classnames(prefixCls, { 238 | [`${prefixCls}-swiping`]: this.state.swiping, 239 | }); 240 | 241 | const refProps = { 242 | ref: el => this.content = ReactDOM.findDOMNode(el), 243 | }; 244 | 245 | return (left!.length || right!.length) && !disabled ? ( 246 |
247 | {/* 保证 body touchStart 后不触发 pan */} 248 |
this.cover = el} /> 249 | { this.renderButtons(left, 'left') } 250 | { this.renderButtons(right, 'right') } 251 | 262 |
{children}
263 |
264 |
265 | ) : ( 266 |
{children}
267 | ); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/index.native.tsx: -------------------------------------------------------------------------------- 1 | export { default } from './Swipeout'; 2 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import Swipeout from './Swipeout'; 2 | 3 | export default Swipeout; 4 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | require('../assets/index.less'); 2 | import './usage'; 3 | -------------------------------------------------------------------------------- /tests/usage.tsx: -------------------------------------------------------------------------------- 1 | import expect from 'expect.js'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import TestUtils from 'react-dom/test-utils'; 5 | // const Simulator = (window as any).Simulator; 6 | import Swipeout from '../src/index'; 7 | 8 | /* global Hammer */ 9 | describe('simple', () => { 10 | let div; 11 | 12 | before(() => { 13 | div = document.createElement('div'); 14 | div.style.width = '320px'; 15 | document.body.insertBefore(div, document.body.firstChild); 16 | }); 17 | 18 | after(() => { 19 | document.body.removeChild(div); 20 | }); 21 | 22 | afterEach(() => { 23 | ReactDOM.unmountComponentAtNode(div); 24 | }); 25 | 26 | it('works when swipe', done => { 27 | const instance = ReactDOM.render( 28 | 38 | swipeout demo 39 | 40 | , div, 41 | ); 42 | const domEl = TestUtils.findRenderedDOMComponentWithClass( 43 | instance as any, 'rc-swipeout-content', 44 | ); 45 | 46 | const rightActionEl = TestUtils.findRenderedDOMComponentWithClass( 47 | instance as any, 'rc-swipeout-actions-right', 48 | ); 49 | 50 | (instance as any).onPanStart({ 51 | direction: 4, 52 | moveStatus: { 53 | x: 10, 54 | }, 55 | }); 56 | (instance as any).onPanMove({ 57 | direction: 4, 58 | moveStatus: { 59 | x: 100, 60 | }, 61 | }); 62 | (instance as any).onPanEnd({ 63 | direction: 4, 64 | moveStatus: { 65 | x: 300, 66 | }, 67 | }); 68 | expect((domEl as any).style.left).to.be('140px'); 69 | expect((rightActionEl as any).offsetWidth).to.be(120); 70 | 71 | const event = document.createEvent('UIEvent'); 72 | event.initEvent('touchstart', true, true); 73 | document.body.dispatchEvent(event); 74 | 75 | expect((domEl as any).style.left).to.be('0px'); 76 | done(); 77 | }); 78 | 79 | it('onOpen & onClose to be called', done => { 80 | let openCalled = false; 81 | let closeCalled = false; 82 | const onOpenSpy = () => { 83 | openCalled = true; 84 | }; 85 | const onCloseSpy = () => { 86 | closeCalled = true; 87 | }; 88 | const instance = ReactDOM.render( 89 | 100 | swipeout demo 101 | 102 | , div, 103 | ); 104 | 105 | (instance as any).onPanStart({ 106 | direction: 4, 107 | moveStatus: { 108 | x: 10, 109 | }, 110 | }); 111 | (instance as any).onPanMove({ 112 | direction: 4, 113 | moveStatus: { 114 | x: 100, 115 | }, 116 | }); 117 | (instance as any).onPanEnd({ 118 | direction: 4, 119 | moveStatus: { 120 | x: 300, 121 | }, 122 | }); 123 | expect(openCalled).to.be(true); 124 | 125 | const event = document.createEvent('UIEvent'); 126 | event.initEvent('touchstart', true, true); 127 | document.body.dispatchEvent(event); 128 | 129 | expect(closeCalled).to.be(true); 130 | done(); 131 | }); 132 | 133 | it('button click & autoClose', done => { 134 | let readCalled = false; 135 | const onRead = () => { 136 | readCalled = true; 137 | }; 138 | const instance = ReactDOM.render( 139 | onRead() }, 142 | { text: 'reply' }, 143 | ]} 144 | autoClose 145 | > 146 | swipeout demo 147 | 148 | , div, 149 | ); 150 | const domEl = TestUtils.findRenderedDOMComponentWithClass( 151 | instance as any, 'rc-swipeout-content', 152 | ); 153 | const BtnElArr = TestUtils.scryRenderedDOMComponentsWithClass( 154 | instance as any, 'rc-swipeout-btn', 155 | ); 156 | 157 | (instance as any).onPanStart({ 158 | direction: 4, 159 | moveStatus: { 160 | x: 10, 161 | }, 162 | }); 163 | (instance as any).onPanMove({ 164 | direction: 4, 165 | moveStatus: { 166 | x: 100, 167 | }, 168 | }); 169 | (instance as any).onPanEnd({ 170 | direction: 4, 171 | moveStatus: { 172 | x: 300, 173 | }, 174 | }); 175 | TestUtils.Simulate.click(BtnElArr[0]); 176 | expect(readCalled).to.be(true); 177 | expect((domEl as any).style.left).to.be('0px'); 178 | done(); 179 | }); 180 | 181 | it('left=right=[] render no swipeout', done => { 182 | const instance = ReactDOM.render( 183 | swipeout demo 184 | , div, 185 | ); 186 | const domElArr = TestUtils.scryRenderedDOMComponentsWithClass( 187 | instance as any, 'rc-swipeout', 188 | ); 189 | expect(domElArr.length).to.be(0); 190 | done(); 191 | }); 192 | 193 | it('render works', done => { 194 | const instance = ReactDOM.render( 195 | 204 | swipeout demo 205 | 206 | , div, 207 | ); 208 | const domElArr = TestUtils.scryRenderedDOMComponentsWithClass( 209 | instance as any, 'rc-swipeout', 210 | ); 211 | expect(domElArr.length).to.be(1); 212 | const actionElArr = TestUtils.scryRenderedDOMComponentsWithClass( 213 | instance as any, 'rc-swipeout-btn', 214 | ); 215 | expect(actionElArr.length).to.be(4); 216 | done(); 217 | }); 218 | 219 | it('swipe when disabled', () => { 220 | const instance = ReactDOM.render( 221 | 228 | swipeout demo 229 | 230 | , div, 231 | ); 232 | const domElArr = TestUtils.scryRenderedDOMComponentsWithClass( 233 | instance as any, 'rc-swipeout-content', 234 | ); 235 | 236 | expect(domElArr.length).to.be(0); 237 | }); 238 | 239 | it('only one side', done => { 240 | const instance = ReactDOM.render( 241 | 247 | swipeout demo 248 | 249 | , div, 250 | ); 251 | 252 | const domEl = TestUtils.findRenderedDOMComponentWithClass( 253 | instance as any, 'rc-swipeout-content', 254 | ); 255 | expect((domEl as any).style.transform).to.be(undefined); 256 | (instance as any).onPanStart({ 257 | direction: 4, 258 | moveStatus: { 259 | x: 10, 260 | }, 261 | }); 262 | (instance as any).onPanMove({ 263 | direction: 2, 264 | moveStatus: { 265 | x: 100, 266 | }, 267 | }); 268 | (instance as any).onPanEnd({ 269 | direction: 2, 270 | moveStatus: { 271 | x: 300, 272 | }, 273 | }); 274 | expect((domEl as any).style.left).to.be('140px'); 275 | done(); 276 | }); 277 | 278 | // don't know why not render, comment temporarily 279 | // it('only right', done => { 280 | // const instance = ReactDOM.render( 281 | // 287 | // swipeout demo 288 | // , 289 | // div, 290 | // ); 291 | // const domEl = TestUtils.findRenderedDOMComponentWithClass( 292 | // instance, 'rc-swipeout-content', 293 | // ); 294 | // const rightActionEl = TestUtils.findRenderedDOMComponentWithClass( 295 | // instance, 'rc-swipeout-actions-right', 296 | // ); 297 | // 298 | // const hammer = new Hammer(domEl, { recognizers: [] }); 299 | // const swipeRight = new Hammer.Swipe({ threshold: 1, direction: Hammer.DIRECTION_RIGHT }); 300 | // hammer.add(swipeRight); 301 | // 302 | // Simulator.gestures.swipe(domEl, { 303 | // deltaX: 300, 304 | // deltaY: 10, 305 | // }, () => { 306 | // expect(parseInt(domEl.style.left, 10)).to.be(0); 307 | // 308 | // // const swipeLeft = new Hammer.Swipe({ threshold: 1, direction: Hammer.DIRECTION_LEFT }); 309 | // // hammer.add(swipeLeft); 310 | // // 311 | // // Simulator.gestures.swipe(domEl, { 312 | // // deltaX: -300, 313 | // // deltaY: 10, 314 | // // }, () => { 315 | // // expect(domEl.style.left).to.be('-128px'); 316 | // // expect(rightActionEl.style.width).to.be('128px'); 317 | // done(); 318 | // // }); 319 | // }); 320 | // }); 321 | }); 322 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strictNullChecks": true, 4 | "moduleResolution": "node", 5 | "jsx": "preserve", 6 | "allowSyntheticDefaultImports":true, 7 | "target": "es6", 8 | "skipLibCheck": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /typings/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module "rc-hammerjs" { 2 | var Ret: any; 3 | export default Ret; 4 | } 5 | 6 | declare module "object.omit" { 7 | var Ret: any; 8 | export default Ret; 9 | } 10 | 11 | declare module "rc-swipeout" { 12 | var Ret: any; 13 | export default Ret; 14 | } 15 | -------------------------------------------------------------------------------- /typings/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | --------------------------------------------------------------------------------