├── .babelrc ├── .coveralls.yml ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── scripts └── rollup.config.js ├── src ├── Swipeable.js ├── __tests__ │ ├── Swipeable.test.js │ └── helpers │ │ ├── Demo.js │ │ └── events.js └── helpers.js └── test-setup.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "loose": true, "modules": false }], 4 | "react" 5 | ], 6 | "plugins": [ 7 | ["transform-class-properties", { "loose": true }] 8 | ], 9 | "env": { 10 | "cjs": { 11 | "plugins": [ 12 | "transform-es2015-modules-commonjs" 13 | ] 14 | }, 15 | "test": { 16 | "plugins": [ 17 | "transform-es2015-modules-commonjs" 18 | ] 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: omE8KuE16ns7iJk6nOsnnaJ779ySEdWUm 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | insert_final_newline = true 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | max_line_length = 80 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "root": true, 3 | "extends": [ 4 | "prettier", 5 | "prettier/react", 6 | "react-app", 7 | "eslint:recommended", 8 | "plugin:react/recommended", 9 | "plugin:flowtype/recommended" 10 | ], 11 | "plugins": [ 12 | "react", 13 | "prettier", 14 | "flowtype", 15 | "cypress" 16 | ], 17 | "parser": "babel-eslint", 18 | "parserOptions": { 19 | "ecmaVersion": 2016, 20 | "sourceType": "module", 21 | "ecmaFeatures": { 22 | "jsx": true 23 | } 24 | }, 25 | "env": { 26 | "browser": true, 27 | "commonjs": true, 28 | "node": true, 29 | "es6": true, 30 | "jest": true, 31 | "mocha": true, 32 | "cypress/globals": true 33 | }, 34 | "settings": { 35 | "flowtype": { 36 | "onlyFilesWithFlowAnnotation": true 37 | } 38 | }, 39 | "rules": { 40 | "no-debugger": "off", 41 | "no-console": "off", 42 | "react/prop-types": "off", 43 | "jsx-a11y/href-no-hash": "off", 44 | "jsx-a11y/anchor-is-valid": ["warn", { "aspects": ["invalidHref"] }], 45 | "no-unused-vars": [ 46 | "error", 47 | { 48 | "args": "after-used", 49 | "ignoreRestSiblings": false 50 | } 51 | ], 52 | "prettier/prettier": [ 53 | "error", 54 | { 55 | "semi": true, 56 | "bracketSpacing": false, 57 | "trailingComma": "es5", 58 | }, 59 | ], 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - stable 5 | 6 | install: 7 | - yarn install 8 | 9 | script: 10 | - yarn test 11 | - yarn cover 12 | 13 | # Send coverage data to Coveralls 14 | after_script: "cat coverage/lcov.info | node_modules/coveralls/bin/coveralls.js" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2009-2014 TJ Holowaychuk 4 | Copyright (c) 2013-2014 Roman Shtylman 5 | Copyright (c) 2014-2015 Douglas Christopher Wilson 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | 'Software'), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Swipy (`react-swipy`) 2 | [![Build Status](https://travis-ci.org/goncy/react-swipy.svg?branch=master)](https://travis-ci.org/goncy/react-swipy) 3 | [![Coverage Status](https://coveralls.io/repos/github/goncy/react-swipy/badge.svg?branch=master)](https://coveralls.io/github/goncy/react-swipy?branch=master) 4 | 5 | A simple Tinder-like swipeable React component 6 | 7 | ## 🚨 This library is not being mantained anymore 🚨 8 | But [@pedro-lb](https://github.com/pedro-lb) ported this to Typescript and is being mantained [here](https://github.com/pedro-lb/react-deck-swiper) 9 | 10 | ## Installation 11 | ```sh 12 | # NPM 13 | npm install --save react-swipy 14 | # Yarn 15 | yarn add react-swipy 16 | ``` 17 | 18 | ## Why 19 | I didn't like the lack of control on mose deck-based swipeable components out there 20 | 21 | ## How 22 | [![See in CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/5x53pnrn3x) 23 | ```jsx 24 | import React, {Component} from "react"; 25 | import Swipeable from "react-swipy" 26 | 27 | import Card from "./components/Card"; 28 | import Button from "./components/Button"; 29 | 30 | const wrapperStyles = {position: "relative", width: "250px", height: "250px"}; 31 | const actionsStyles = { 32 | display: "flex", 33 | justifyContent: "space-between", 34 | marginTop: 12, 35 | }; 36 | 37 | class App extends Component { 38 | state = { 39 | cards: ["First", "Second", "Third"], 40 | }; 41 | 42 | remove = () => 43 | this.setState(({cards}) => ({ 44 | cards: cards.slice(1, cards.length), 45 | })); 46 | 47 | render() { 48 | const {cards} = this.state; 49 | 50 | return ( 51 |
52 |
53 | {cards.length > 0 ? ( 54 |
55 | ( 57 |
58 | 59 | 60 |
61 | )} 62 | onAfterSwipe={this.remove} 63 | > 64 | {cards[0]} 65 |
66 | {cards.length > 1 && {cards[1]}} 67 |
68 | ) : ( 69 | No more cards 70 | )} 71 |
72 |
73 | ); 74 | } 75 | } 76 | 77 | export default App; 78 | ``` 79 | 80 | ## Props 81 | 82 | #### limit 83 | `Number` 84 | 85 | Offset in px swiped to consider as swipe 86 | 87 | #### min 88 | `Number` 89 | 90 | Offset when opacity fade should start 91 | 92 | #### onBeforeSwipe 93 | `Function` 94 | 95 | Callback executed before swiping, it receives 3 parameters: 96 | * A function that, when called, executes the swipe ('left' or 'right' can be passed to force direction) 97 | * A function that will cancel the swipe 98 | * The direction of the swipe 99 | 100 | #### onSwipe 101 | `Function` 102 | 103 | Callback executed right after swipe, it receives 'left' or 'right' as first parameter 104 | 105 | #### onAfterSwipe 106 | `Function` 107 | 108 | Callback executed when animation ends 109 | 110 | #### children 111 | `Node` 112 | 113 | Content of the card 114 | 115 | #### buttons 116 | `Function` 117 | 118 | Function that returns a `left` and `right` function that will force a swipe 119 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-swipy", 3 | "author": "Gonzalo Pozzo ", 4 | "version": "0.0.5", 5 | "license": "ISC", 6 | "description": "React swipeable component", 7 | "main": "dist/react-swipy.min.js", 8 | "keywords": [ 9 | "react", 10 | "tinder", 11 | "swipe", 12 | "swipeable", 13 | "card", 14 | "deck", 15 | "swiper", 16 | "swipy" 17 | ], 18 | "scripts": { 19 | "build": "PACKAGE_NAME=react-swipy rollup --config scripts/rollup.config.js", 20 | "test": "jest", 21 | "cover": "npm test -- --coverage" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git://github.com/goncy/react-swipy.git" 26 | }, 27 | "files": [ 28 | "README.md", 29 | "dist/" 30 | ], 31 | "devDependencies": { 32 | "babel-core": "^6.26.3", 33 | "babel-eslint": "^8.2.3", 34 | "babel-jest": "^23.0.0", 35 | "babel-plugin-external-helpers": "^6.22.0", 36 | "babel-plugin-transform-class-properties": "^6.24.1", 37 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", 38 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 39 | "babel-preset-env": "^1.7.0", 40 | "babel-preset-react": "^6.24.1", 41 | "change-case": "^3.0.2", 42 | "coveralls": "^3.0.1", 43 | "enzyme": "^3.3.0", 44 | "enzyme-adapter-react-16": "^1.1.1", 45 | "eslint": "^4.19.1", 46 | "eslint-config-prettier": "^2.9.0", 47 | "eslint-config-react-app": "^2.1.0", 48 | "eslint-plugin-cypress": "^2.0.1", 49 | "eslint-plugin-flowtype": "^2.49.3", 50 | "eslint-plugin-import": "^2.12.0", 51 | "eslint-plugin-jsx-a11y": "^6.0.3", 52 | "eslint-plugin-prettier": "^2.6.0", 53 | "eslint-plugin-react": "^7.9.1", 54 | "jest": "^23.0.0", 55 | "prettier": "^1.13.5", 56 | "react-test-renderer": "^16.4.0", 57 | "rollup": "^0.59.1", 58 | "rollup-plugin-babel": "^3.0.4", 59 | "rollup-plugin-commonjs": "^9.1.3", 60 | "rollup-plugin-node-resolve": "^3.3.0", 61 | "rollup-plugin-replace": "^2.0.0", 62 | "rollup-plugin-uglify": "^4.0.0" 63 | }, 64 | "dependencies": { 65 | "react": "^16.4.0", 66 | "react-dom": "^16.4.0", 67 | "react-spring": "^5.3.8" 68 | }, 69 | "jest": { 70 | "setupTestFrameworkScriptFile": "/test-setup.js", 71 | "verbose": true, 72 | "collectCoverage": true, 73 | "notify": true, 74 | "collectCoverageFrom": [ 75 | "**/src/*.{js,jsx}" 76 | ], 77 | "testPathIgnorePatterns": [ 78 | "/node_modules/", 79 | "/__tests__/helpers" 80 | ] 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /scripts/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve' 2 | import babel from 'rollup-plugin-babel' 3 | import replace from 'rollup-plugin-replace' 4 | import commonjs from 'rollup-plugin-commonjs' 5 | import { uglify } from 'rollup-plugin-uglify' 6 | 7 | const PACKAGE_NAME = process.env.PACKAGE_NAME 8 | const ENTRY_FILE = "./src/Swipeable.js" 9 | const OUTPUT_DIR = "./dist" 10 | const EXTERNAL = ['react-spring', 'react'] 11 | const GLOBALS = { 12 | react: 'React' 13 | } 14 | 15 | const isExternal = id => !id.startsWith('.') && !id.startsWith('/') 16 | 17 | export default [ 18 | { 19 | input: ENTRY_FILE, 20 | output: { 21 | file: `${OUTPUT_DIR}/${PACKAGE_NAME}.umd.js`, 22 | format: 'umd', 23 | name: PACKAGE_NAME, 24 | globals: GLOBALS, 25 | }, 26 | external: EXTERNAL, 27 | plugins: [ 28 | resolve(), 29 | babel(), 30 | commonjs(), 31 | replace({ 'process.env.NODE_ENV': JSON.stringify('development') }), 32 | ], 33 | }, 34 | 35 | { 36 | input: ENTRY_FILE, 37 | output: { 38 | file: `${OUTPUT_DIR}/${PACKAGE_NAME}.min.js`, 39 | format: 'umd', 40 | name: PACKAGE_NAME, 41 | globals: GLOBALS, 42 | }, 43 | external: EXTERNAL, 44 | plugins: [ 45 | resolve(), 46 | babel(), 47 | commonjs(), 48 | replace({ 'process.env.NODE_ENV': JSON.stringify('production') }), 49 | uglify(), 50 | ], 51 | }, 52 | 53 | { 54 | input: ENTRY_FILE, 55 | output: { 56 | file: `${OUTPUT_DIR}/${PACKAGE_NAME}.cjs.js`, 57 | format: 'cjs', 58 | }, 59 | external: isExternal, 60 | plugins: [babel()], 61 | }, 62 | 63 | { 64 | input: ENTRY_FILE, 65 | output: { 66 | file: `${OUTPUT_DIR}/${PACKAGE_NAME}.esm.js`, 67 | format: 'es', 68 | }, 69 | external: isExternal, 70 | plugins: [babel()], 71 | }, 72 | ] 73 | -------------------------------------------------------------------------------- /src/Swipeable.js: -------------------------------------------------------------------------------- 1 | import React, {PureComponent, Fragment} from "react"; 2 | import {Spring} from "react-spring"; 3 | import { 4 | getDirection, 5 | getOpacity, 6 | getOffset, 7 | withX, 8 | getLimitOffset, 9 | } from "./helpers"; 10 | 11 | const SWIPE_CONFIG = { 12 | tension: 390, 13 | friction: 30, 14 | restSpeedThreshold: 1, 15 | restDisplacementThreshold: 0.01, 16 | overshootClamping: true, 17 | lastVelocity: 1, 18 | mass: 0.1, 19 | }; 20 | 21 | const DEFAULT_PROPS = { 22 | limit: 120, 23 | min: 40, 24 | }; 25 | 26 | const INITIAL_STATE = { 27 | start: 0, 28 | offset: 0, 29 | forced: false, 30 | swiped: false, 31 | moving: false, 32 | pristine: true, 33 | }; 34 | 35 | export default class Swipeable extends PureComponent { 36 | static defaultProps = DEFAULT_PROPS; 37 | 38 | state = INITIAL_STATE; 39 | 40 | componentDidMount() { 41 | window.addEventListener("touchmove", this.onDragMove); 42 | window.addEventListener("mousemove", this.onDragMove); 43 | window.addEventListener("touchend", this.onDragEnd); 44 | window.addEventListener("mouseup", this.onDragEnd); 45 | } 46 | 47 | componentWillUnmount() { 48 | window.removeEventListener("touchmove", this.onDragMove); 49 | window.removeEventListener("mousemove", this.onDragMove); 50 | window.removeEventListener("touchend", this.onDragEnd); 51 | window.removeEventListener("mouseup", this.onDragEnd); 52 | } 53 | 54 | onDragStart = withX(start => { 55 | if (this.state.swiped) return; 56 | 57 | this.setState({start, pristine: false, moving: true}); 58 | }); 59 | 60 | onDragMove = withX(end => { 61 | const {start, swiped, moving} = this.state; 62 | 63 | if (swiped || !moving) return; 64 | 65 | this.setState({offset: getOffset(start, end)}); 66 | }); 67 | 68 | onDragEnd = () => { 69 | const {offset, swiped, moving} = this.state; 70 | const {limit} = this.props; 71 | 72 | if (swiped || !moving) return; 73 | 74 | if (Math.abs(offset) >= limit) { 75 | this.onBeforeSwipe(getDirection(offset)); 76 | } else { 77 | this.onCancelSwipe(); 78 | } 79 | }; 80 | 81 | onCancelSwipe = () => this.setState({start: 0, offset: 0, moving: false}); 82 | 83 | onBeforeSwipe = direction => { 84 | const {onBeforeSwipe} = this.props; 85 | 86 | if (onBeforeSwipe) { 87 | onBeforeSwipe( 88 | _direction => this.onSwipe(_direction || direction), 89 | this.onCancelSwipe, 90 | direction 91 | ); 92 | } else { 93 | this.onSwipe(direction); 94 | } 95 | }; 96 | 97 | onSwipe = direction => { 98 | const {limit, onSwipe} = this.props; 99 | 100 | if (onSwipe) { 101 | onSwipe(direction); 102 | } 103 | 104 | this.setState({ 105 | swiped: true, 106 | moving: false, 107 | offset: getLimitOffset(limit, direction), 108 | }); 109 | }; 110 | 111 | onAfterSwipe = () => { 112 | const {onAfterSwipe} = this.props; 113 | 114 | this.setState(INITIAL_STATE); 115 | 116 | if (onAfterSwipe) { 117 | onAfterSwipe(); 118 | } 119 | }; 120 | 121 | forceSwipe = direction => { 122 | if (this.state.swiped) return; 123 | 124 | this.setState({ 125 | pristine: false, 126 | forced: true, 127 | }); 128 | 129 | this.onBeforeSwipe(direction); 130 | }; 131 | 132 | render() { 133 | const {offset, swiped, pristine, forced} = this.state; 134 | const {children, limit, buttons, min} = this.props; 135 | 136 | return ( 137 | 138 | swiped && this.onAfterSwipe()} 145 | immediate={pristine || (!forced && Math.abs(offset) >= limit)} 146 | config={SWIPE_CONFIG} 147 | > 148 | {({offset, opacity}) => ( 149 |
159 | {children} 160 |
161 | )} 162 |
163 | {buttons && 164 | buttons({ 165 | right: () => this.forceSwipe("right"), 166 | left: () => this.forceSwipe("left"), 167 | })} 168 |
169 | ); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/__tests__/Swipeable.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {mount} from "enzyme"; 3 | 4 | import Swipeable from "../Swipeable"; 5 | import {getOffset} from "../helpers"; 6 | 7 | import Demo from "./helpers/Demo"; 8 | import {getState, simulate, swipe, wait} from "./helpers/events"; 9 | 10 | describe("Lifecycles", () => { 11 | const wrapper = mount( 12 | 13 | Hello 14 | 15 | ); 16 | 17 | it("Should have content", () => { 18 | expect(wrapper.find('[data-test="content"]').length).toBe(1); 19 | }); 20 | 21 | it("Should unmount correctly", () => { 22 | wrapper.unmount(); 23 | 24 | expect(wrapper.find('[data-test="content"]').length).toBe(0); 25 | }); 26 | }); 27 | 28 | describe("Buttons", () => { 29 | const wrapper = mount( 30 | ( 32 |
33 | 36 | 39 |
40 | )} 41 | /> 42 | ); 43 | 44 | it("Should remove a card after an accept click", async () => { 45 | wrapper.find('[data-test="right"]').simulate("click"); 46 | 47 | await wait(600); 48 | 49 | expect(wrapper.find('[data-test="content"]').text()).toEqual("Second"); 50 | }); 51 | 52 | it("Should remove a card after a reject click", async () => { 53 | wrapper.find('[data-test="left"]').simulate("click"); 54 | 55 | await wait(600); 56 | 57 | expect(wrapper.find('[data-test="content"]').text()).toEqual("Third"); 58 | }); 59 | 60 | it("Should wait to remove a card to remove the next one", async () => { 61 | wrapper.find('[data-test="left"]').simulate("click"); 62 | wrapper.find('[data-test="left"]').simulate("click"); 63 | 64 | await wait(600); 65 | 66 | expect(wrapper.find('[data-test="content"]').text()).toEqual("Fourth"); 67 | }); 68 | }); 69 | 70 | describe("onBeforeSwipe", () => { 71 | let shouldSwipe = true; 72 | let direction = null; 73 | 74 | const wrapper = mount( 75 | 77 | shouldSwipe ? (direction ? swipe(direction) : swipe()) : cancel() 78 | } 79 | buttons={({right, left}) => ( 80 |
81 | 84 | 87 |
88 | )} 89 | /> 90 | ); 91 | 92 | it("Should remove a card after forcing swipe", async () => { 93 | wrapper.find('[data-test="left"]').simulate("click"); 94 | 95 | await wait(600); 96 | 97 | expect(wrapper.find('[data-test="content"]').text()).toEqual("Second"); 98 | }); 99 | 100 | it("Should cancel swipe correctly", async () => { 101 | shouldSwipe = false; 102 | 103 | wrapper.find('[data-test="left"]').simulate("click"); 104 | 105 | await wait(600); 106 | 107 | expect(wrapper.find('[data-test="content"]').text()).toEqual("Second"); 108 | }); 109 | 110 | it("Can force the swipe direction", async () => { 111 | shouldSwipe = true; 112 | direction = "right"; 113 | 114 | wrapper.find('[data-test="left"]').simulate("click"); 115 | 116 | await wait(600); 117 | 118 | expect(wrapper.find('[data-test="content"]').text()).toEqual("Third"); 119 | expect(wrapper.find('[data-test="direction"]').text()).toEqual("right"); 120 | }); 121 | }); 122 | 123 | describe("Double swiping", () => { 124 | const wrapper = mount(Hello); 125 | 126 | it("Should have the same state after clicking twice", async () => { 127 | simulate(wrapper, "forceSwipe", "left"); 128 | const first = getState(wrapper); 129 | 130 | await wait(100); 131 | 132 | simulate(wrapper, "forceSwipe", "right"); 133 | const second = getState(wrapper); 134 | 135 | expect(first).toMatchObject(second); 136 | }); 137 | 138 | it("Should not swipe manually as soon as the button is clicked", async () => { 139 | simulate(wrapper, "forceSwipe", "left"); 140 | const first = getState(wrapper); 141 | 142 | await wait(100); 143 | 144 | swipe(wrapper, 250); 145 | const second = getState(wrapper); 146 | 147 | expect(first).toMatchObject(second); 148 | }); 149 | }); 150 | 151 | describe("Cancel", () => { 152 | const onSwipe = jest.fn(); 153 | const wrapper = mount( 154 | 155 | Hello 156 | 157 | ); 158 | 159 | it("Should not swipe if the limit wasn't reached", async () => { 160 | swipe(wrapper, 200); 161 | 162 | expect(onSwipe).toHaveBeenCalledTimes(0); 163 | }); 164 | }); 165 | 166 | describe("Swipe", () => { 167 | it("Should remove a card after touch swipe", async () => { 168 | const onSwipe = jest.fn(); 169 | const wrapper = mount(Hello); 170 | 171 | expect(getState(wrapper)).toMatchObject({ 172 | start: 0, 173 | moving: false, 174 | pristine: true, 175 | }); 176 | 177 | wrapper.simulate("touchStart", { 178 | touches: [ 179 | { 180 | pageX: 500, 181 | pageY: 0, 182 | }, 183 | ], 184 | }); 185 | 186 | expect(getState(wrapper)).toMatchObject({ 187 | start: 500, 188 | moving: true, 189 | pristine: false, 190 | }); 191 | 192 | simulate(wrapper, "onDragMove", { 193 | touches: [ 194 | { 195 | pageX: 0, 196 | pageY: 0, 197 | }, 198 | ], 199 | }); 200 | 201 | expect(getState(wrapper)).toMatchObject({ 202 | start: 500, 203 | offset: getOffset(500, 0), 204 | moving: true, 205 | pristine: false, 206 | }); 207 | 208 | simulate(wrapper, "onDragEnd"); 209 | 210 | expect(onSwipe).toHaveBeenCalledTimes(1); 211 | expect(onSwipe).toHaveBeenCalledWith("left"); 212 | }); 213 | 214 | it("Should remove a card after mouse swipe", async () => { 215 | const onSwipe = jest.fn(); 216 | const wrapper = mount(); 217 | 218 | expect(getState(wrapper)).toMatchObject({ 219 | start: 0, 220 | moving: false, 221 | pristine: true, 222 | }); 223 | 224 | wrapper.simulate("mouseDown", { 225 | pageX: 0, 226 | pageY: 0, 227 | }); 228 | 229 | expect(getState(wrapper)).toMatchObject({ 230 | start: 0, 231 | moving: true, 232 | pristine: false, 233 | }); 234 | 235 | simulate(wrapper, "onDragMove", { 236 | pageX: 500, 237 | pageY: 0, 238 | }); 239 | 240 | expect(getState(wrapper)).toMatchObject({ 241 | start: 0, 242 | offset: getOffset(0, 500), 243 | moving: true, 244 | pristine: false, 245 | }); 246 | 247 | simulate(wrapper, "onDragEnd"); 248 | 249 | expect(onSwipe).toHaveBeenCalledTimes(1); 250 | expect(onSwipe).toHaveBeenCalledWith("right"); 251 | }); 252 | }); 253 | 254 | describe("onAfterSwipe", () => { 255 | it("Should call a handler after swiping", done => { 256 | const onAfterSwipe = jest.fn(() => { 257 | expect(onAfterSwipe).toHaveBeenCalledTimes(1); 258 | done(); 259 | }); 260 | 261 | const wrapper = mount( 262 | Hello 263 | ); 264 | 265 | swipe(wrapper, 500); 266 | }); 267 | }); 268 | -------------------------------------------------------------------------------- /src/__tests__/helpers/Demo.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { render } from "react-dom"; 3 | 4 | import Swipeable from "../../Swipeable" 5 | 6 | const wrapperStyles = {position: "relative", width: "250px", height: "250px"}; 7 | const actionsStyles = { 8 | display: "flex", 9 | justifyContent: "space-between", 10 | marginTop: 12, 11 | }; 12 | 13 | export default class Demo extends Component { 14 | state = { 15 | cards: ["First", "Second", "Third", "Fourth"], 16 | direction: '' 17 | }; 18 | 19 | remove = () => 20 | this.setState(({cards}) => ({ 21 | cards: cards.slice(1, cards.length), 22 | })); 23 | 24 | setDirection = direction => this.setState({direction}) 25 | 26 | render() { 27 | const {cards, direction} = this.state; 28 | 29 | return ( 30 |
31 |
32 | {cards.length > 0 ? ( 33 |
34 | 39 |
{cards[0]}
40 |
41 | {cards.length > 1 &&
{cards[1]}
} 42 |
43 | ) : ( 44 |
No more cards
45 | )} 46 |
47 |
{direction}
48 |
49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/__tests__/helpers/events.js: -------------------------------------------------------------------------------- 1 | export function getState(wrapper) { 2 | return wrapper.instance().state; 3 | } 4 | 5 | export function simulate(wrapper, method, ...values) { 6 | return wrapper.instance()[method](...values); 7 | } 8 | 9 | export function swipe(wrapper, offset) { 10 | wrapper.simulate("mouseDown", { 11 | pageX: offset, 12 | pageY: 0, 13 | }); 14 | 15 | simulate(wrapper, "onDragMove", { 16 | pageX: 0, 17 | pageY: 0, 18 | }); 19 | 20 | simulate(wrapper, "onDragEnd"); 21 | } 22 | 23 | export function wait(time) { 24 | return new Promise(_ => setTimeout(_, time)); 25 | } 26 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | export const getDirection = offset => (offset > 0 ? "right" : "left"); 2 | export const getOffset = (start, end) => -((start - end) * 0.75); 3 | export const getEvent = e => (e.touches ? e.touches[0] : e); 4 | export const withX = fn => e => fn(getEvent(e).pageX); 5 | export const getLimitOffset = (limit, direction) => 6 | direction === "right" ? limit : -limit; 7 | export const getOpacity = (offset, limit, min) => 8 | 1 - 9 | (Math.abs(offset) < min 10 | ? 0 11 | : (Math.abs(offset) - min) / Math.abs(limit - min)); 12 | -------------------------------------------------------------------------------- /test-setup.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | configure({ adapter: new Adapter() }); --------------------------------------------------------------------------------