├── .changeset └── config.json ├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── ---support-usage-question.md │ └── bug_report.md └── workflows │ ├── ci.yml │ ├── release.yml │ └── size.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __tests__ ├── __snapshots__ │ └── useSwipeable.spec.tsx.snap ├── helpers │ └── index.ts └── useSwipeable.spec.tsx ├── docs ├── .gitignore ├── README.md ├── babel.config.js ├── docs │ ├── api.md │ ├── examples │ │ ├── examples.md │ │ ├── feature-test-console.mdx │ │ ├── simple-carousel.mdx │ │ └── simple-pattern.mdx │ ├── faq.md │ ├── introduction.mdx │ └── v7-migration.md ├── docusaurus.config.ts ├── package.json ├── sidebars.ts ├── src │ ├── components │ │ ├── examples │ │ │ ├── FeatureTestConsole │ │ │ │ ├── SwipeableHook.tsx │ │ │ │ ├── TableComponents.tsx │ │ │ │ └── index.tsx │ │ │ ├── SimpleCarousel │ │ │ │ ├── Carousel.tsx │ │ │ │ └── index.tsx │ │ │ ├── SimplePattern │ │ │ │ ├── index.tsx │ │ │ │ └── pattern.tsx │ │ │ ├── SwipeDemo │ │ │ │ └── index.tsx │ │ │ ├── components.tsx │ │ │ └── images.tsx │ │ └── landing │ │ │ ├── divider.tsx │ │ │ ├── landing-banner.tsx │ │ │ ├── landing-featured-projects.tsx │ │ │ ├── landing-features.tsx │ │ │ ├── landing-hero.tsx │ │ │ ├── landing-images.tsx │ │ │ └── nf-link-button.tsx │ ├── css │ │ └── custom.css │ └── pages │ │ └── index.tsx ├── static │ ├── .nojekyll │ ├── font │ │ ├── InterBold.woff2 │ │ ├── InterMedium.woff2 │ │ └── InterRegular.woff2 │ └── img │ │ ├── feature-1.png │ │ ├── feature-2.png │ │ ├── feature-3.png │ │ ├── hero-pattern.png │ │ ├── nearform-icon-white.svg │ │ ├── nearform-icon.svg │ │ ├── nearform-logo-white.svg │ │ ├── nearform-logo.svg │ │ ├── product-1.jpg │ │ ├── product-2.jpg │ │ ├── product-3.jpg │ │ ├── product-4.jpg │ │ └── product-5.jpg ├── tailwind.config.ts ├── tsconfig.json └── yarn.lock ├── examples ├── README.md ├── app │ ├── App.tsx │ ├── FeatureTestConsole │ │ ├── SwipeableHook.tsx │ │ ├── TableComponents.tsx │ │ └── index.tsx │ ├── SimpleCarousel │ │ ├── Carousel.tsx │ │ └── index.tsx │ ├── SimplePattern │ │ ├── index.tsx │ │ └── pattern.tsx │ └── components.tsx ├── css │ └── foundation.min.css ├── index.html ├── index.tsx ├── package.json ├── server.js ├── tsconfig.json ├── webpack.config.js ├── webpack.config.min.js └── yarn.lock ├── package.json ├── react-swipeable-Hero.png ├── rollup.config.js ├── src ├── index.ts └── types.ts ├── tsconfig.all.json ├── tsconfig.base.json ├── tsconfig.json ├── tsconfig.test.json ├── tsconfig.types.json └── yarn.lock /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": [ 4 | "@svitejs/changesets-changelog-github-compact", 5 | { 6 | "repo": "FormidableLabs/react-swipeable" 7 | } 8 | ], 9 | "access": "public", 10 | "baseBranch": "main" 11 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | es 3 | dist 4 | lib 5 | examples 6 | docs/docusaurus.config.ts 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "project": "./tsconfig.all.json", 5 | "ecmaFeatures": { 6 | "jsx": true 7 | } 8 | }, 9 | "extends": [ 10 | "eslint:recommended", 11 | "plugin:@typescript-eslint/eslint-recommended", 12 | "plugin:@typescript-eslint/recommended", 13 | "plugin:react/recommended", 14 | "prettier", 15 | "prettier/@typescript-eslint" 16 | ], 17 | "plugins": ["@typescript-eslint", "react-hooks"], 18 | "settings": { 19 | "react": { 20 | "version": "detect" 21 | } 22 | }, 23 | "rules": { 24 | "@typescript-eslint/no-unused-vars": [ 25 | "error", 26 | { "varsIgnorePattern": "_" } 27 | ], 28 | "@typescript-eslint/explicit-function-return-type": "off", 29 | "@typescript-eslint/no-empty-function": "off", 30 | "@typescript-eslint/no-explicit-any": "off", 31 | "@typescript-eslint/ban-ts-ignore": "off", 32 | "react/no-unescaped-entities": "off", 33 | "react/prop-types": "off", 34 | "react-hooks/rules-of-hooks": "error", 35 | "react-hooks/exhaustive-deps": "warn" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---support-usage-question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F914 Support/Usage Question" 3 | about: General support for swipeable 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | If you have a question that doesn't concern bugs or requests, all i am asking you is to look for answers first before you hit submit. 11 | 12 | When you haven't found anything, please isolate your issue in a [codesandbox](https://codesandbox.io/), that makes it so much easier on our side to inspect and understand the issue. 13 | 14 | Fork the example codesandbox: 15 | - https://codesandbox.io/s/react-swipeable-image-carousel-hben8 16 | 17 | 18 | Thanks for understanding! 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Steps or Sandbox to reproduce** 14 | Fork the example codesandbox: 15 | - https://codesandbox.io/s/react-swipeable-image-carousel-hben8 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | ** Device/Browser ** 21 | Please let us know what device and/or browser has the issue. 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # Runs build and test on: 4 | # every push that has a change in a file not in the docs or examples folder 5 | # every pull request with main branch as the base that has a change in a file not in the docs or examples folder 6 | on: 7 | push: 8 | branches: 9 | - main 10 | paths: 11 | - '**' 12 | - '!docs/**' 13 | - '!examples/**' 14 | pull_request: 15 | branches: 16 | - main 17 | paths: 18 | - '**' 19 | - '!docs/**' 20 | - '!examples/**' 21 | 22 | jobs: 23 | check_and_build: 24 | name: Check and build codebase 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: actions/setup-node@v4 29 | with: 30 | cache: 'yarn' 31 | node-version: 18 32 | 33 | - name: Installation 34 | run: yarn --prefer-offline --frozen-lockfile --non-interactive 35 | 36 | - name: Check Code 37 | run: yarn check:ci 38 | 39 | - name: Build Core 40 | run: yarn build 41 | 42 | - name: Check size 43 | run: yarn size 44 | 45 | code_coverage: 46 | name: Check code coverage 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v4 50 | - uses: actions/setup-node@v4 51 | with: 52 | cache: 'yarn' 53 | node-version: 18 54 | 55 | - name: Installation 56 | run: yarn --prefer-offline --frozen-lockfile --non-interactive 57 | 58 | - name: Build Code Coverage 59 | run: yarn test:cover 60 | 61 | - name: Coveralls 62 | uses: coverallsapp/github-action@master 63 | with: 64 | github-token: ${{ secrets.GITHUB_TOKEN }} 65 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | id-token: write 13 | issues: write 14 | repository-projects: write 15 | deployments: write 16 | packages: write 17 | pull-requests: write 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | token: ${{ secrets.CHANGESETS_TOKEN }} 22 | 23 | - uses: actions/setup-node@v4 24 | with: 25 | cache: 'yarn' 26 | node-version: 18 27 | 28 | - name: Install dependencies 29 | run: yarn install --frozen-lockfile 30 | 31 | - name: Build 32 | run: yarn build 33 | 34 | - name: Unit Tests 35 | run: yarn test 36 | 37 | - name: PR or Publish 38 | id: changesets 39 | uses: changesets/action@v1 40 | with: 41 | version: yarn changeset version 42 | publish: yarn changeset publish 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.CHANGESETS_TOKEN }} 45 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 46 | -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: size 2 | on: [pull_request] 3 | jobs: 4 | size: 5 | runs-on: ubuntu-latest 6 | env: 7 | CI_JOB_NUMBER: 1 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: andresz1/size-limit-action@v1 11 | with: 12 | github_token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | TODO 3 | *.log 4 | .DS_Store 5 | .vscode 6 | es 7 | lib 8 | dist 9 | examples/static 10 | coverage 11 | package-lock.json 12 | build 13 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | node_modules 3 | /es 4 | /lib 5 | /dist 6 | /coverage 7 | /examples 8 | *.mdx -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v7.0.0 2 | 3 | ## 7.0.2 4 | 5 | ### Patch Changes 6 | 7 | - Add react v19 to peer deps ([#357](https://github.com/FormidableLabs/react-swipeable/pull/357)) 8 | 9 | ## 7.0.1 10 | 11 | ### Patch Changes 12 | 13 | - Adding GitHub release workflow ([#328](https://github.com/FormidableLabs/react-swipeable/pull/328)) 14 | **New Features:** 15 | - add new `swipeDuration` prop - "allowable duration of a swipe" 16 | - A swipe lasting more than `swipeDuration`, in milliseconds, will **not** be considered a swipe. 17 | - Feature mimicked from `use-gesture` [swipe.duration](https://use-gesture.netlify.app/docs/options/#swipeduration) 18 | - Defaults to `Infinity` for backwards compatibility 19 | - add new `touchEventOptions` prop that can set the options for the touch event listeners 20 | - this provides users full control of if/when they want to set [passive](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options) 21 | - Defaults to `{ passive: true }` 22 | - add new `onTouchStartOrOnMouseDown` prop that is called for `touchstart` and `mousedown`. Before a swipe even starts. 23 | - combined with `touchEventOptions` allows users the ability to now call `preventDefault` on `touchstart` 24 | - add new `onTouchEndOrOnMouseUp` prop that is called for `touchend` and `mouseup`. 25 | - add [react 18](https://reactjs.org/blog/2022/03/29/react-v18.html) to `peerDependencies` 26 | 27 | **Breaking Changes:** 28 | 29 | - we have dropped support for `es5` transpiled output 30 | - we target `es2015` for our transpilation now 31 | - `swipeable` utilizes object/array spread & const/let natively 32 | - `preventScrollOnSwipe` - "new" prop. Replaces `preventDefaultTouchmoveEvent` 33 | - same functionality but renamed to be more explicit on its intended use 34 | - **fixed bug** - where toggling this prop did not re-attach event listeners 35 | - **update** - we now **only** change the `passive` event listener option for `touchmove` depending on this prop 36 | - see notes in README for more details [readme#passive-listener](https://github.com/FormidableLabs/react-swipeable#passive-listener) 37 | - Thank you [@stefvhuynh](https://github.com/stefvhuynh) 38 | 39 | **Bug fixes:** 40 | 41 | - fix bug where directional swiped check allowed `undefined`/falsy values to set `cancelablePageSwipe` 42 | - Thank you [@bhj](https://github.com/bhj) for the [comment](https://github.com/FormidableLabs/react-swipeable/pull/240#issuecomment-1014980025) 43 | - fix bug when both `trackTouch` and `trackMouse` were present that triggered an erroneous swipe when the user clicked outside and above the swipeable area 44 | - See [issue 304](https://github.com/FormidableLabs/react-swipeable/issues/304) for details 45 | - Thank you [@Sacret](https://github.com/Sacret) 46 | 47 | **Infrastructure:** 48 | 49 | - post `size-limit report` to PRs with bundle diff sizes 50 | - utilize `rollup` for build & output 51 | - remove dependency on `microbundle` 52 | - remove `interop` injected code - [pull/260](https://github.com/FormidableLabs/react-swipeable/pull/260#discussion_r679541081) 53 | - Thank you [@binoy14](https://github.com/binoy14) 54 | - upgrade lots of dev dependencies 55 | - 🎉 upgrade to `typescript` `v4.6.3` 56 | - export/outputs housekeeping and cleaning (mimicked from `react-redux`) 57 | - removed/renamed exports from `package.json`: 58 | - `browser`, `umd:main`(renamed `dist`), `jsnext:main`(use `module`), `typings`(use `types`) 59 | - moved exports - **old** => **new** 60 | - `"main": "./dist/react-swipeable.js"` => `"main": "./lib/index.js"` 61 | - `"module": "./dist/react-swipeable.module.js"` => `"module": "es/index.js"` 62 | - `"types": "./dist/index.d.ts"` => `"types": "./es/index.d.ts"` 63 | 64 | # v6.2.2 65 | 66 | - add react v18 to `peerDependencies` 67 | 68 | # v6.2.1 69 | 70 | - Fix issue with some config properties being set to `undefined` breaking swipeable 71 | - [PR #296](https://github.com/formidablelabs/react-swipeable/pull/296) 72 | - explicitly set `undefined` config props to config defaults 73 | - Thank you [@simonflk](https://github.com/simonflk) 74 | 75 | # v6.2.0 76 | 77 | - `delta` prop can now be an `object` specifying different values for each direction 78 | - [PR #260](https://github.com/formidablelabs/react-swipeable/pull/260) 79 | - Thank you [@macabeus](https://github.com/macabeus) for the idea and PR! 80 | - defaults `delta` if direction not present in object, [PR #262](https://github.com/formidablelabs/react-swipeable/pull/262) 81 | - Maintenance 82 | - upgrade to latest version of `microbundle` 83 | - remove comments from built files 84 | - attempt to lower size to counter unnecessary increase from `microbundle` upgrade due to `rollup` `output.interop` 85 | - include type updates influenced by [PR #259](https://github.com/formidablelabs/react-swipeable/pull/259) 86 | - Thank you [@jaketodaro](https://github.com/jaketodaro) 87 | - ~dependabot security updates~ 88 | 89 | # v6.1.2 90 | 91 | - Maintenance 92 | - actually include dependabot security updates 93 | - update badge links 94 | 95 | # v6.1.1 96 | 97 | - Maintenance 98 | - ~dependabot security updates~ 99 | - Migrate to github actions, remove travis, update badges 100 | - Update examples and provide link via codesandbox 101 | 102 | # v6.1.0 103 | 104 | - Add new event handler prop `onSwipeStart` 105 | 106 | - called only once per swipe at the start and before the first `onSwiping` callback 107 | - The `first` property of the `SwipeEventData` will be `true` 108 | - [PR #226](https://github.com/formidablelabs/react-swipeable/pull/226) 109 | - Thank you [@feketegy](https://github.com/feketegy) for the idea! 110 | 111 | - **typescript** updated to `v4.1.3` and associated deps bumped to be compatible 112 | - [PR #228](https://github.com/formidablelabs/react-swipeable/pull/228) 113 | - Thank you [@cwise89](https://github.com/cwise89)! 114 | 115 | # v6.0.1 116 | 117 | - Fix issue with `first` property on `SwipeEventData` always being `true`. 118 | - `first` is now only `true` for the first event, then `false` for subsequent events 119 | - [issue #221](https://github.com/formidablelabs/react-swipeable/issues/221) and [PR #223](https://github.com/formidablelabs/react-swipeable/pull/223) 120 | - Thank you [@feketegy](https://github.com/feketegy)! 121 | 122 | # v6.0.0 123 | 124 | **New Features:** 125 | 126 | - include passive event listener option, by default, to internal uses of `addEventListener` 127 | - solves issue with chrome and lighthouse - [#167](https://github.com/FormidableLabs/react-swipeable/issues/167) 128 | - set `passive` to `false` only when `preventDefaultTouchmoveEvent` is `true`. 129 | - more details in [readme#passive-listener-issue](https://github.com/FormidableLabs/react-swipeable#passive-listener) 130 | - add new `onTap` event handler prop which executes its callback after a tap 131 | - Thank you [@upatel32](https://github.com/upatel32)! 132 | - add new `vxvy` event data property 133 | - `[ deltaX/time, deltaY/time]` - velocity per axis 134 | - Thank you [@upatel32](https://github.com/upatel32)! 135 | 136 | **Breaking Changes:** 137 | 138 | - **remove** `` component 139 | - see below for an example of how to make your own 140 | - [Swipeable component examples](https://github.com/FormidableLabs/react-swipeable/blob/main/migration.md#swipeable-component-examples) 141 | - **event data update** correctly calculate `deltaX` and `deltaY` 142 | - from `initial - current` **to** `current - initial` 143 | - fixes issue [#157](https://github.com/FormidableLabs/react-swipeable/issues/157) 144 | - Thank you [@upatel32](https://github.com/upatel32)! 145 | - **drop support for ie11** 146 | - using `addEventListener` options object needs to be polyfilled, [browser support](https://github.com/FormidableLabs/react-swipeable#browser-support) 147 | - **requires** react >= 16.8.3, additionally supports new react v17 148 | 149 | **Bug fixes:** 150 | 151 | - Swipes can now start at edges (x or y === 0) 152 | - fixes [#182](https://github.com/FormidableLabs/react-swipeable/issues/182) 153 | - Thank you [@upatel32](https://github.com/upatel32)! 154 | 155 | **Infrastructure:** 156 | 157 | - **typescript** Converted entire code base, tests, and examples to typescript 158 | - **changed type** `EventData` -> `SwipeEventData` - The event data provided for all swipe event callbacks 159 | - **removed type** `SwipeableOptions` - use `SwipeableProps` now 160 | - **removed types** associated with `` component 161 | - **new type** `TapCallback` - callback for the new `onTap` prop handler 162 | - **new type** `SwipeDirections` - `"Left" | "Right" | "Up" | "Down"` 163 | - Converted tests to `@testing-library/react`, [react testing library](https://github.com/testing-library/react-testing-library) 164 | - Build bundles with `microbundle`. [microbundle](https://github.com/developit/microbundle) 165 | - export new "modern" build - via package.json `esmodule` property 166 | - [microbundle modern mode](https://github.com/developit/microbundle#-modern-mode-) 167 | 168 | **Maintenance:** 169 | 170 | - Upgraded all dev dependencies, `jest`, `babel`, `webpack`, `eslint`, `prettier` 171 | 172 | # 5.5.0 173 | 174 | - Add `first` property to `eventData` that is `true` for first swipe event [issue #160](https://github.com/formidablelabs/react-swipeable/issues/160) and [PR #162](https://github.com/formidablelabs/react-swipeable/pull/162) 175 | - Thank you [@samanpwbb](https://github.com/samanpwbb)! 176 | 177 | # 5.4.0 178 | 179 | - Add `initial` property to `eventData` that supplies the inital `[x, y]` swipe value coordinates [issue #150](https://github.com/formidablelabs/react-swipeable/issues/150) and [PR #131](https://github.com/formidablelabs/react-swipeable/pull/151) 180 | 181 | # 5.3.0 182 | 183 | - Optimization for `useSwipeable` hook. Added `useMemo` for handler internals [issue #134](https://github.com/formidablelabs/react-swipeable/issues/134) and [PR #149](https://github.com/formidablelabs/react-swipeable/pull/149) 184 | - Thank you [@FaberVitale](https://github.com/FaberVitale)! 185 | 186 | # 5.2.3 187 | 188 | - Add check for `event.cancelable` for `touchmove` events before calling `event.preventDefault()`, [issue #128](https://github.com/formidablelabs/react-swipeable/issues/128) and [PR #145](https://github.com/formidablelabs/react-swipeable/pull/145) 189 | - Thank you [@maurispalletti](https://github.com/maurispalletti)! 190 | 191 | # 5.2.2 192 | 193 | - Fix typescript types for `ref`, [issue #140](https://github.com/formidablelabs/react-swipeable/issues/140) and [PR #142](https://github.com/formidablelabs/react-swipeable/pull/142) 194 | - Thank you [@mastermatt](https://github.com/mastermatt)! 195 | 196 | # 5.2.0 197 | 198 | - Fix bug where callbacks/props were not refreshed for `useSwipeable` and ``, [issue #136](https://github.com/formidablelabs/react-swipeable/issues/136) and [PR #138](https://github.com/formidablelabs/react-swipeable/pull/138) 199 | - Thank you [@caesarsol](https://github.com/caesarsol) and [@bas-l](https://github.com/bas-l)! 200 | - Add typescript types for `useSwipeable` and ``, [issue #125](https://github.com/formidablelabs/react-swipeable/issues/125) 201 | - Thank you [@adambowles](https://github.com/adambowles)! 202 | 203 | # 5.1.0 204 | 205 | - Fix for `preventDefaultTouchmoveEvent` in safari [issue #127](https://github.com/formidablelabs/react-swipeable/issues/127) and [PR #131](https://github.com/formidablelabs/react-swipeable/pull/131) 206 | - Thank you [@JiiB](https://github.com/JiiB) and [@bhj](https://github.com/bhj)! 207 | - use `ref` callback for both `` and `useSwipeable` to attach all touch event handlers 208 | - `useSwipeable`'s returned `handlers` now contains a ref callback 209 | - Please see disscusion and comments in both [#127](https://github.com/formidablelabs/react-swipeable/issues/127) and [#131](https://github.com/formidablelabs/react-swipeable/issues/127) for more details and info. 210 | - fix avoids the `passive: true` issue from chrome document event listeners 211 | - fix avoids bug on safari where the `touchmove` event listener needs to be attached before a `touchstart` in order to be able to call `e.preventDefault` 212 | - removed `touchHandlerOption` prop 213 | - fix above deprecates this prop 214 | 215 | # 5.0.0 216 | 217 | - Introduce react hook, `useSwipeable` 218 | - Core rewrite to simplify api and trim down bundled size 219 | - Add `size-limit` to help keep bundled size down 220 | - Add `es` export via `"module": "es/index.js"` to `package.json` 221 | - Add `prettier` code formating 222 | - **[BREAKING]** simplify handler event data to allow destructuring 223 | - `onSwiped = ({ event, direction, absX, absY, velocity}) => console.log('swiped')` 224 | - **[BREAKING]** deprecated `onSwiping{Left|Right|Up|Down}` handler props 225 | - can be replaced with direction/`dir` event data 226 | - `` onSwiping = ({ dir }) => console.log(`swiping - ${dir}`) `` 227 | - **[BREAKING]** deprecated props 228 | - `flickThreshold` 229 | - `stopPropagation` 230 | - `disabled` 231 | - **[BREAKING]** deprecated passing "rest" of props down 232 | - removed additional props besides the ones used by `` from being passed down 233 | - only `className` and `style` get passed to ``'s dom node, default `div` 234 | 235 | # 4.3.0 236 | 237 | - Add `rotationAngle` prop. [#103](https://github.com/formidablelabs/react-swipeable/pull/103) 238 | - will allow to set a rotation angle, e.g. for a four-player game on a tablet, where each player has a 90° turned view. 239 | - Thank you [@Narquadah](https://github.com/Narquadah) and [@LarsKumbier](https://github.com/LarsKumbier)! 240 | 241 | # 4.2.2 242 | 243 | - fixed bug that happened when if either `onSwiping` or `onSwiped` were set we were not calling `e.preventDefault()` appropriately 244 | 245 | # 4.2.0 246 | 247 | - Add support for calling `preventDefault` on Chrome 56+ via passive event support checking and manual event listener setup. [#88](https://github.com/formidablelabs/react-swipeable/pull/88) 248 | - Thank you [@kl0tl](https://github.com/kl0tl) and [@KrashStudio](https://github.com/KrashStudio)! 249 | 250 | # 4.1.0 251 | 252 | - add `disabled` prop. [#83](https://github.com/formidablelabs/react-swipeable/pull/83) 253 | - add `innerRef` prop that allows user to access to ``'s inner dom node react ref. [#82](https://github.com/formidablelabs/react-swipeable/pull/82) 254 | 255 | # 4.0.1 256 | 257 | - fixed bug where delta was causing a swipe to not be tracked correctly, #74 , thanks @mctep 258 | 259 | # 4.0.0 260 | 261 | - **Major Change** `preventDefaultTouchmoveEvent` defaults to `false` now [#69](https://github.com/formidablelabs/react-swipeable/issue/69) 262 | - This change is in part due to a [Chrome56+ change](https://github.com/formidablelabs/react-swipeable#chrome-56-and-later-warning-with-preventdefault) 263 | - **Major Change** drop support for React 12 & 13, `peerDependencies` updated [#64](https://github.com/formidablelabs/react-swipeable/pull/64) 264 | - `prop-types` added to `dependencies` [#64](https://github.com/formidablelabs/react-swipeable/pull/64) 265 | - **Major Change** `trackMouse` now 'tracks' the swipe outside of the swipeable component, [#67](https://github.com/formidablelabs/react-swipeable/pull/67). 266 | - Thanks for example [@TanaseHagi](https://github.com/TanaseHagi) 267 | - react 16 added to `peerDependencies` 268 | 269 | # 3.9.0 270 | 271 | - add `onTap` functionality. Thanks [@anicke](https://github.com/anicke) . [#61](https://github.com/formidablelabs/react-swipeable/pull/61) [#39](https://github.com/formidablelabs/react-swipeable/issues/39) 272 | - added persisting synthetic event in example via `e.persist()`. This should help people see more details in the console when debugging in the [example](http://stack.formidable.com/react-swipeable/. 273 | 274 | # 3.8.0 275 | 276 | - Allow `onMouseDown`, `onMouseUp`, and `onMouseMove` props to fire appropriately again. [#55](https://github.com/formidablelabs/react-swipeable/pull/55), thanks [@lochstar](https://github.com/lochstar) 277 | - Stop using this.state to track swipes, thanks [@grantila](https://github.com/grantila) for pointing out this change and submitting PR, [#58](https://github.com/formidablelabs/react-swipeable/pull/58). Should provide minor performance gains since `Swipeable` will no longer be calling `this.setState` internally. 278 | 279 | # 3.7.0 280 | 281 | - add ability to track mouse events as touch events. Thanks [@jakepusateri](https://github.com/jakepusateri) and [@Marcel-G](https://github.com/Marcel-G). [#51](https://github.com/formidablelabs/react-swipeable/issues/51) 282 | 283 | # 3.6.0 284 | 285 | - add stopPropagation prop for all swipe events, defaults to `false`. See [#46](https://github.com/formidablelabs/react-swipeable/issues/46) for more info. 286 | 287 | # 3.5.1 288 | 289 | - fix React 15.2.0 warning for unknown properties on DOM elements 290 | 291 | # 3.5.0 292 | 293 | - Add configurable container element via `nodeName` prop, defaults to `'div'`. See [#24](https://github.com/formidablelabs/react-swipeable/issues/24) and [#40](https://github.com/formidablelabs/react-swipeable/pull/40) for more info. 294 | 295 | # 3.4.0 296 | 297 | - Add preventDefault while swiping when props `onSwipedLeft`, `onSwipedRight`, `onSwipedUp`, and `onSwipedDown` are present. See [#21](https://github.com/formidablelabs/react-swipeable/issues/21) and [#37](https://github.com/formidablelabs/react-swipeable/pull/37) for more info. 298 | 299 | # 3.3.0 300 | 301 | - Adds `velocity` data to `onSwiping` callback 302 | - Updated the build process introducing ES2015 and babel 303 | 304 | # 3.2.0 305 | 306 | - Adds `preventDefaultTouchMoveEvent` option, defaults to true 307 | 308 | # 3.1.0 309 | 310 | - Adds `isFLick` to onSwipe events 311 | - Removes React as a peer dep 312 | - Adds onSwiping events 313 | 314 | # 3.0.2 315 | 316 | - Fixes onSwipeDown and onSwipeUp events 317 | 318 | # 3.0.1 319 | 320 | - Fixes vertical swiping 321 | 322 | # 3.0.0 323 | 324 | - Refactors build into jsx. 325 | 326 | # 2.1.0 327 | 328 | - Adds onSwipedUp, onSwipedRight, onSwipedDown, onSwipedLeft callbacks. 329 | 330 | # 2.0 331 | 332 | - `onFlick` prop has been removed. 333 | 334 | - `onSwipe` now has a 4th argument for the callback `Boolean isFlick` 335 | 336 | - Added a prop `flickThreshold` which allows you to customize at what velocity a flick is detected. 337 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Contributor Covenant Code of Conduct 2 | 3 | ### Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | ### Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ### Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ### Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ### Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at coc@formidable.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ### Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 71 | version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for contributing! 4 | 5 | ## Before you contribute 6 | 7 | This package tries to keep a small footprint/package size by limiting its scope and concerns. It's mainly used as a building block for more complex custom features. 8 | 9 | With this in mind feature requests and PRs that greatly expand its scope will likely not move forward, but we are open to discussing them. 10 | 11 | We encourage pull requests concerning: 12 | 13 | - Bugs in this library 14 | - New tests for React 15 | - Documentation 16 | 17 | ## Development 18 | 19 | Initial install & setup, with **node 16** & **yarn v1**, run `yarn install`. 20 | 21 | Make changes/updates to the `src/index.ts` file. 22 | 23 | **_Please add/update tests if PR adds/changes functionality._** 24 | 25 | ### Verify updates with the examples 26 | 27 | Build, run, and test examples locally: 28 | 29 | ```sh 30 | # Yarn install 31 | yarn install 32 | # Run the start:dev:local command 33 | yarn start:examples:local 34 | ``` 35 | 36 | Then open a browser tab to `http://localhost:8080/`. 37 | 38 | You can now make updates/changes to `src/index.ts` and webpack will rebuild, then reload the page so you can test your changes! 39 | 40 | ## Testing 41 | 42 | Our builds run unit tests, lint, prettier, compile/build, and watch package size via [size-limit](https://github.com/ai/size-limit/). 43 | 44 | All these steps can be verified via a single command. 45 | 46 | ```sh 47 | # validate all the things 48 | yarn test 49 | ``` 50 | 51 | ## Documentation 52 | 53 | Documentation is managed within the `docs` directory. In order to test and preview changes, you'll require Node 18+ to run locally. 54 | 55 | ```sh 56 | # navigate to docs directory 57 | cd docs 58 | # install modules 59 | docs$ yarn install 60 | # start locally and view at http://localhost:3000/ 61 | docs$ yarn start 62 | ``` 63 | 64 | ### Unit Testing 65 | 66 | All unit tests are located in: 67 | 68 | - `__tests__/useSwipeable.spec.tsx` 69 | 70 | These run in node using `jest` and [@testing-library/react](https://github.com/testing-library/react-testing-library). 71 | 72 | ```sh 73 | # run all tests 74 | $ yarn run test:unit 75 | 76 | # run all tests and watch for changes 77 | $ yarn run test:unit:watch 78 | ``` 79 | 80 | ### Lint & Formatting 81 | 82 | We've attempted to standardize our lint and format with `eslint` and `prettier`. The build will fail if these fail. 83 | 84 | ```sh 85 | # run lint 86 | $ yarn run lint 87 | 88 | # run prettier 89 | $ yarn run prettier 90 | 91 | # fix prettier errors & reformat code 92 | $ yarn run format 93 | ``` 94 | 95 | If you see this error: 96 | 97 | ``` 98 | [warn] Code style issues found in the above file(s). Forgot to run Prettier? 99 | ``` 100 | 101 | Then run the formatter: 102 | 103 | ```sh 104 | $ yarn run format 105 | ``` 106 | 107 | ### Using changesets 108 | 109 | Our official release path is to use automation to perform the actual publishing of our packages. The steps are to: 110 | 111 | 1. A human developer adds a changeset. Ideally this is as a part of a PR that will have a version impact on a package. 112 | 2. On merge of a PR our automation system opens a "Version Packages" PR. 113 | 3. On merging the "Version Packages" PR, the automation system publishes the packages. 114 | 115 | Here are more details: 116 | 117 | ### Add a changeset 118 | 119 | When you would like to add a changeset (which creates a file indicating the type of change), in your branch/PR issue this command: 120 | 121 | ```sh 122 | $ yarn changeset 123 | ``` 124 | 125 | to produce an interactive menu. Navigate the packages with arrow keys and hit `` to select 1+ packages. Hit `` when done. Select semver versions for packages and add appropriate messages. From there, you'll be prompted to enter a summary of the change. Some tips for this summary: 126 | 127 | 1. Aim for a single line, 1+ sentences as appropriate. 128 | 2. Include issue links in GH format (e.g. `#123`). 129 | 3. You don't need to reference the current pull request or whatnot, as that will be added later automatically. 130 | 131 | After this, you'll see a new uncommitted file in `.changesets` like: 132 | 133 | ```sh 134 | $ git status 135 | # .... 136 | Untracked files: 137 | (use "git add ..." to include in what will be committed) 138 | .changeset/flimsy-pandas-marry.md 139 | ``` 140 | 141 | Changeset will use a randomly generated file name for the markdown description file. 142 | 143 | Review the file, make any necessary adjustments, and commit it to source. When we eventually do a package release, the changeset notes and version will be incorporated! 144 | 145 | ### Creating versions 146 | 147 | On a merge of a feature PR, the changesets GitHub action will open a new PR titled `"Version Packages"`. This PR is automatically kept up to date with additional PRs with changesets. So, if you're not ready to publish yet, just keep merging feature PRs and then merge the version packages PR later. 148 | 149 | ### Publishing packages 150 | 151 | On the merge of a version packages PR, the changesets GitHub action will publish the packages to npm. 152 | 153 | ## Project Maintainers 154 | 155 | ### Manual publish method 156 | 157 | 158 | ### Releasing a new version 159 | 1. Publish to npm 160 | 2. Update version in `examples` 161 | 162 | #### 1. Publish to npm 163 | 164 | ```sh 165 | # (1) Runs tests, lint, build published dir, updates package.json 166 | $ npm version [patch|minor|major|] 167 | 168 | # (2) If all is well, publish the new version to the npm registry 169 | $ npm publish 170 | 171 | # (3) Then, update github and push new associated tag 172 | $ git push --follow-tags 173 | ``` 174 | 175 | #### 2. Update version in the examples 176 | 177 | After publishing a new version to npm we need to make sure the `examples` get updated. 178 | 179 | 1. Bump the `react-swipeable` version in `examples/package.json` to the new just released version 180 | 2. Run `yarn` to install and update the lock file 181 | 3. Push changes to `main` branch so the codesandbox examples get updated 182 | 4. Build and deploy updated examples to github pages 183 | 184 | 185 | 186 | ### Building and deploying examples to github pages 187 | 188 | The examples build using the most recent version of `react-swipeable`. 189 | 190 | Make sure you've already completed the above steps for `Update version in the examples` so the `examples` have the most recent version installed. 191 | 192 | (Optional) Validate examples build locally 193 | 194 | ```sh 195 | # From root - build the examples 196 | $ yarn examples:build 197 | 198 | # cd into examples and start simple http server(python v3) 199 | # validate everything works locally: http://localhost:8080/ 200 | $ cd examples 201 | examples$ python -m http.server 8080 202 | ``` 203 | 204 | ```sh 205 | # From root - build and publish the examples app to github pages 206 | $ yarn examples:build:publish 207 | 208 | 209 | ``` 210 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) 2014-2022 Josh Perez 4 | Copyright (C) 2014-2022 Brian Emil Hartz 5 | Copyright (C) 2022 Formidable Labs, Inc. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![React Swipeable — Formidable, We build the modern web](https://raw.githubusercontent.com/FormidableLabs/react-swipeable/main/react-swipeable-Hero.png)](https://formidable.com/open-source/) 2 | 3 | React swipe event handler hook 4 | 5 | [![npm downloads](https://img.shields.io/npm/dm/react-swipeable.svg)](https://www.npmjs.com/package/react-swipeable) [![npm version](https://img.shields.io/npm/v/react-swipeable.svg)](https://www.npmjs.com/package/react-swipeable) [![build status](https://github.com/FormidableLabs/react-swipeable/actions/workflows/ci.yml/badge.svg)](https://github.com/FormidableLabs/react-swipeable/actions) [![gzip size](https://badgen.net/bundlephobia/minzip/react-swipeable)](https://bundlephobia.com/result?p=react-swipeable) [![maintenance status](https://img.shields.io/badge/maintenance-active-green.svg)](https://github.com/FormidableLabs/react-swipeable#maintenance-status) 6 | 7 | [![Edit react-swipeable image carousel](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/s/github/FormidableLabs/react-swipeable/tree/main/examples?file=/app/SimpleCarousel/Carousel.tsx) 8 | 9 | Visit the [Docs site](https://commerce.nearform.com/open-source/react-swipeable) for information on [usage](https://commerce.nearform.com/open-source/react-swipeable/docs/usage), [api](https://commerce.nearform.com/open-source/react-swipeable/api), and [demos](https://commerce.nearform.com/open-source/react-swipeable/docs/demo). 10 | 11 | ## License 12 | 13 | [MIT]((./LICENSE)) 14 | 15 | ## Contributing 16 | 17 | Please see our [contributions guide](./CONTRIBUTING.md). 18 | 19 | ### Maintainers 20 | [Project Maintenance](./CONTRIBUTING.md#project-maintainers) 21 | 22 | ## Maintenance Status 23 | 24 | **Active:** Formidable is actively working on this project, and we expect to continue for work for the foreseeable future. Bug reports, feature requests and pull requests are welcome. 25 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/useSwipeable.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`useSwipeable handles mouse events with trackMouse prop and fires correct props: useSwipeable onSwiped trackMouse 1`] = ` 4 | Object { 5 | "absX": 100, 6 | "absY": 0, 7 | "deltaX": 100, 8 | "deltaY": 0, 9 | "dir": "Right", 10 | "event": MouseEvent { 11 | "isTrusted": false, 12 | }, 13 | "first": false, 14 | "initial": Array [ 15 | 100, 16 | 100, 17 | ], 18 | "velocity": Any, 19 | "vxvy": Any, 20 | } 21 | `; 22 | 23 | exports[`useSwipeable handles mouse events with trackMouse prop and fires correct props: useSwipeable onSwiping trackMouse 1`] = ` 24 | Object { 25 | "absX": 25, 26 | "absY": 0, 27 | "deltaX": 25, 28 | "deltaY": 0, 29 | "dir": "Right", 30 | "event": MouseEvent { 31 | "isTrusted": false, 32 | }, 33 | "first": true, 34 | "initial": Array [ 35 | 100, 36 | 100, 37 | ], 38 | "velocity": Any, 39 | "vxvy": Any, 40 | } 41 | `; 42 | 43 | exports[`useSwipeable handles touch events and fires correct props: useSwipeable onSwiped trackTouch 1`] = ` 44 | Object { 45 | "absX": 0, 46 | "absY": 100, 47 | "deltaX": 0, 48 | "deltaY": 100, 49 | "dir": "Down", 50 | "event": TouchEvent { 51 | "isTrusted": false, 52 | }, 53 | "first": false, 54 | "initial": Array [ 55 | 100, 56 | 100, 57 | ], 58 | "velocity": Any, 59 | "vxvy": ArrayContaining [ 60 | Any, 61 | Any, 62 | ], 63 | } 64 | `; 65 | 66 | exports[`useSwipeable handles touch events and fires correct props: useSwipeable onSwiping trackTouch 1`] = ` 67 | Object { 68 | "absX": 0, 69 | "absY": 25, 70 | "deltaX": 0, 71 | "deltaY": 25, 72 | "dir": "Down", 73 | "event": TouchEvent { 74 | "isTrusted": false, 75 | }, 76 | "first": true, 77 | "initial": Array [ 78 | 100, 79 | 100, 80 | ], 81 | "velocity": Any, 82 | "vxvy": ArrayContaining [ 83 | Any, 84 | Any, 85 | ], 86 | } 87 | `; 88 | -------------------------------------------------------------------------------- /__tests__/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export type MockedSwipeFunctions = { 2 | onSwiping: jest.Mock; 3 | onSwiped: jest.Mock; 4 | onSwipedLeft: jest.Mock; 5 | onSwipedRight: jest.Mock; 6 | onSwipedUp: jest.Mock; 7 | onSwipedDown: jest.Mock; 8 | }; 9 | 10 | const expectSwipingDir = (fns: jest.Mock, dir: string) => { 11 | fns.mock.calls.forEach((call) => { 12 | expect(call[0].dir).toBe(dir); 13 | }); 14 | }; 15 | 16 | export const expectSwipeFuncsDir = ( 17 | sf: MockedSwipeFunctions, 18 | dir: string 19 | ): void => { 20 | Object.keys(sf).forEach((s) => { 21 | if (s.endsWith(dir) || s === "onSwiped") { 22 | expect(sf[s as keyof MockedSwipeFunctions]).toHaveBeenCalled(); 23 | } else if (s === "onSwiping") { 24 | expectSwipingDir(sf[s], dir); 25 | } else { 26 | expect(sf[s as keyof MockedSwipeFunctions]).not.toHaveBeenCalled(); 27 | } 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # React-Swipeable documentation site 2 | 3 | This is the documentation site for React-Swipeable. 4 | 5 | This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. 6 | 7 | ### Installation 8 | 9 | ``` 10 | $ yarn 11 | ``` 12 | 13 | ### Local Development 14 | 15 | ``` 16 | $ yarn start 17 | ``` 18 | 19 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 20 | 21 | ### Build 22 | 23 | ``` 24 | $ yarn build 25 | ``` 26 | 27 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 28 | 29 | ### Deployment 30 | 31 | This site is deployed using Vercel, which will automatically detect the site config and deploy 32 | -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/docs/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | ## Props / Config Options 4 | 5 | ### Event handler props 6 | 7 | ```js 8 | { 9 | onSwiped, // After any swipe (SwipeEventData) => void 10 | onSwipedLeft, // After LEFT swipe (SwipeEventData) => void 11 | onSwipedRight, // After RIGHT swipe (SwipeEventData) => void 12 | onSwipedUp, // After UP swipe (SwipeEventData) => void 13 | onSwipedDown, // After DOWN swipe (SwipeEventData) => void 14 | onSwipeStart, // Start of swipe (SwipeEventData) => void *see details* 15 | onSwiping, // During swiping (SwipeEventData) => void 16 | onTap, // After a tap ({ event }) => void 17 | 18 | // Pass through callbacks, event provided: ({ event }) => void 19 | onTouchStartOrOnMouseDown, // Called for `touchstart` and `mousedown` 20 | onTouchEndOrOnMouseUp, // Called for `touchend` and `mouseup` 21 | } 22 | ``` 23 | 24 | #### Details 25 | - `onSwipeStart` - called only once per swipe at the start and before the first `onSwiping` callback 26 | - The `first` property of the `SwipeEventData` will be `true` 27 | 28 | ### Configuration props and default values 29 | 30 | ```js 31 | { 32 | delta: 10, // min distance(px) before a swipe starts. *See Notes* 33 | preventScrollOnSwipe: false, // prevents scroll during swipe (*See Details*) 34 | trackTouch: true, // track touch input 35 | trackMouse: false, // track mouse input 36 | rotationAngle: 0, // set a rotation angle 37 | swipeDuration: Infinity, // allowable duration of a swipe (ms). *See Notes* 38 | touchEventOptions: { passive: true }, // options for touch listeners (*See Details*) 39 | } 40 | ``` 41 | 42 | #### delta 43 | 44 | `delta` can be either a `number` or an `object` specifying different deltas for each direction, [`left`, `right`, `up`, `down`], direction values are optional and will default to `10`; 45 | 46 | ```js 47 | { 48 | delta: { up: 20, down: 20 } // up and down ">= 20", left and right default to ">= 10" 49 | } 50 | ``` 51 | 52 | #### swipeDuration 53 | A swipe lasting more than `swipeDuration`, in milliseconds, will **not** be considered a swipe. 54 | - It will also **not** trigger any callbacks and the swipe event will stop being tracked 55 | - **Defaults** to `Infinity` for backwards compatibility, a sensible duration could be something like `250` 56 | - Feature mimicked from `use-gesture` [swipe.duration](https://use-gesture.netlify.app/docs/options/#swipeduration) 57 | 58 | ```js 59 | { 60 | swipeDuration: 250 // only swipes under 250ms will trigger callbacks 61 | } 62 | ``` 63 | 64 | #### touchEventOptions 65 | 66 | Allows the user to set the options for the touch event listeners( currently only `passive` option ). 67 | - `touchstart`, `touchmove`, and `touchend` event listeners 68 | - **Defaults** to `{ passive: true }` 69 | - this provides users full control of if/when they want to set [passive](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options) 70 | - https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options 71 | - `preventScrollOnSwipe` option **supersedes** `touchEventOptions.passive` for `touchmove` event listener 72 | - See `preventScrollOnSwipe` for [more details](#preventscrollonswipe-details) 73 | 74 | ## Swipe Event Data 75 | 76 | All Event Handlers are called with the below event data, `SwipeEventData`. 77 | 78 | ```js 79 | { 80 | event, // source event 81 | initial, // initial swipe [x,y] 82 | first, // true for the first event of a tracked swipe 83 | deltaX, // x offset (current.x - initial.x) 84 | deltaY, // y offset (current.y - initial.y) 85 | absX, // absolute deltaX 86 | absY, // absolute deltaY 87 | velocity, // √(absX^2 + absY^2) / time - "absolute velocity" (speed) 88 | vxvy, // [ deltaX/time, deltaY/time] - velocity per axis 89 | dir, // direction of swipe (Left|Right|Up|Down) 90 | } 91 | ``` 92 | 93 | **None of the props/config options are required.** 94 | 95 | ### Hook details 96 | 97 | - Hook use requires **react >= 16.8.3** 98 | - The props contained in `handlers` are currently `ref` and `onMouseDown` 99 | - Please spread `handlers` as the props contained in it could change as react changes event listening capabilities 100 | 101 | ### `preventScrollOnSwipe` details 102 | 103 | This prop prevents scroll during swipe in most cases. Use this to **stop scrolling** in the browser while a user swipes. 104 | 105 | Swipeable will call `e.preventDefault()` internally in an attempt to stop the browser's [touchmove](https://developer.mozilla.org/en-US/docs/Web/Events/touchmove) event default action (mostly scrolling). 106 | 107 | **NOTE:** `preventScrollOnSwipe` option **supersedes** `touchEventOptions.passive` for the `touchmove` event listener 108 | 109 | **Example scenario:** 110 | > If a user is swiping right with props `{ onSwipedRight: userSwipedRight, preventScrollOnSwipe: true }` then `e.preventDefault()` will be called, but if the user was swiping left then `e.preventDefault()` would **not** be called. 111 | 112 | `e.preventDefault()` is only called when: 113 | - `preventScrollOnSwipe: true` 114 | - `trackTouch: true` 115 | - the users current swipe has an associated `onSwiping` or `onSwiped` handler/prop 116 | 117 | Please experiment with the [Feature Testing Console](examples/feature-test-console) to test `preventScrollOnSwipe`. 118 | 119 | #### passive listener details 120 | Swipeable adds the passive event listener option, by default, to **internal uses** of touch `addEventListener`'s. We set the `passive` option to `false` only when `preventScrollOnSwipe` is `true` and only to `touchmove`. Other listeners will retain `passive: true`. 121 | 122 | **When `preventScrollOnSwipe` is:** 123 | - `true` => `el.addEventListener('touchmove', cb, { passive: false })` 124 | - `false` => `el.addEventListener('touchmove', cb, { passive: true })` 125 | 126 | Here is more information on react's long running passive [event issue](https://github.com/facebook/react/issues/6436). 127 | 128 | We previously had issues with chrome lighthouse performance deducting points for not having passive option set so it is now on by default except in the case mentioned above. 129 | 130 | If, however, you really **need** _all_ of the listeners to be passive (for performance reasons or otherwise), you can prevent all scrolling on the swipeable container by using the `touch-action` css property instead, [see an example](faq#how-to-use-touch-action-to-prevent-scrolling). -------------------------------------------------------------------------------- /docs/docs/examples/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## Static example 4 | 5 | ```jsx 6 | const handlers = useSwipeable({ 7 | onSwiped: (eventData) => console.log("User Swiped!", eventData), 8 | ...config, 9 | }); 10 | 11 | return
Swipe here
; 12 | ``` 13 | This explanation and example borrowed from `use-gesture`'s [wonderful docs](https://use-gesture.netlify.app/docs/extras/#touch-action). 14 | 15 | ## Dynamic example 16 | 17 | ```jsx 18 | const MySwipeableComponent = props => { 19 | const [stopScroll, setStopScroll] = useState(false); 20 | 21 | const handlers = useSwipeable({ 22 | onSwipeStart: () => setStopScroll(true), 23 | onSwiped: () => setStopScroll(false) 24 | }); 25 | 26 | return
Swipe here
; 27 | }; 28 | ``` 29 | 30 | This is a somewhat contrived example as the final outcome would be similar to the static example. However, there may be cases where you want to determine when the user can scroll based on the user's swiping action along with any number of variables from state and props. 31 | -------------------------------------------------------------------------------- /docs/docs/examples/feature-test-console.mdx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import FeatureTestConsole from '@site/src/components/examples/FeatureTestConsole' 4 | 5 | # Feature Test Console 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/docs/examples/simple-carousel.mdx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import SimpleCarousel from '@site/src/components/examples/SimpleCarousel' 4 | 5 | # Simple Carousel 6 | 7 | Below is an example implementation of a simple carousel which utilizes the hooks provided by `react-swipeable` within a TypeScript context. 8 | 9 | 10 | ## Simple Carousel Code Source 11 | 12 | You can see this full example as pure code within the [Carousel.tsx](https://github.com/FormidableLabs/react-swipeable/blob/main/examples/app/SimpleCarousel/Carousel.tsx) file within the React-Swipeable repo directly. 13 | 14 | ## Simple Carousel Live Preview 15 | 16 | 17 | 18 | Note: The action of swiping must have a duration of `500ms` or lower in order to trigger the swipe action. 19 | 20 | ## Simple Carousel Code Explained 21 | 22 | Import the hook directly from the `react-swipeable` library. In our example, we built and imported a local set of UI components: you can utilize your own UI and styling, or use your favorite UI component library of choice. 23 | 24 | ```typescript 25 | import { useSwipeable } from 'react-swipeable'; 26 | import { 27 | Wrapper, 28 | CarouselContainer, 29 | CarouselSlot, 30 | SlideButtonContainer, 31 | SlideButton, 32 | PREV, 33 | NEXT 34 | } from '../components'; 35 | ``` 36 | 37 | Below, we set up types and an interface for some of the work we'll be building. Next, we write a function called `getOrder`, which will drive the position of each item in the carousel, and what order of position each will be displayed in context of the carousel. Finally, we have a simple `getInitialState` position that sets the `CarouselState` of the carousel we'll be building. 38 | 39 | ```typescript 40 | type Direction = typeof PREV | typeof NEXT; 41 | 42 | interface CarouselState { 43 | pos: number; 44 | sliding: boolean; 45 | dir: Direction; 46 | } 47 | 48 | type CarouselAction = 49 | | { type: Direction, numItems: number } 50 | | { type: 'stopSliding' }; 51 | 52 | 53 | const getOrder = (index: number, pos: number, numItems: number) => { 54 | return index - pos < 0 ? numItems - Math.abs(index - pos) : index - pos; 55 | }; 56 | 57 | const getInitialState = (numItems: number): CarouselState => ({ pos: numItems - 1, sliding: false, dir: NEXT }); 58 | ``` 59 | 60 | Next, we build a reducer for controlling the action of the Carousel, using a switch to set `CarouselState` logic. 61 | 62 | ```typescript 63 | function reducer(state: CarouselState, action: CarouselAction): CarouselState { 64 | switch (action.type) { 65 | case PREV: 66 | return { 67 | ...state, 68 | dir: PREV, 69 | sliding: true, 70 | pos: state.pos === 0 ? action.numItems - 1 : state.pos - 1 71 | }; 72 | case NEXT: 73 | return { 74 | ...state, 75 | dir: NEXT, 76 | sliding: true, 77 | pos: state.pos === action.numItems - 1 ? 0 : state.pos + 1 78 | }; 79 | case 'stopSliding': 80 | return { ...state, sliding: false }; 81 | default: 82 | return state; 83 | } 84 | } 85 | ``` 86 | 87 | Then, building upon the reducer logic, the `` is constructed. We hold the number of items within a count of `numItems`. We utilize the reducer within the `React.useReducer` hook. 88 | 89 | By creating `slide`, as a `const`, we can utilize that later in the component, calling it within `useSwipeable`: called upon `slide(NEXT)` and `slide(PREV)`, invoking the `dispatch` and the `timeout` we built within `slide`. Within the use of `useSwipeable`, we set `swipeDuration` to `500ms`. We set `preventScrollOnSwipe` to `true`, and `trackMouse` to `true`. 90 | 91 | At the end, we return the component itself, built with the components we've created, with `handlers` passed into the wrapping `
` around the surrounding container. The `` holds the directional and sliding state, and within that container the items we want to display are mapped as `React.Children`, utilizing `getOrder`. 92 | 93 | When we put it all together, our `` is complete! 94 | 95 | ```typescript 96 | const Carousel: FunctionComponent<{children: ReactNode}> = (props) => { 97 | const numItems = React.Children.count(props.children); 98 | const [state, dispatch] = React.useReducer(reducer, getInitialState(numItems)); 99 | 100 | const slide = (dir: Direction) => { 101 | dispatch({ type: dir, numItems }); 102 | setTimeout(() => { 103 | dispatch({ type: 'stopSliding' }); 104 | }, 50); 105 | }; 106 | 107 | const handlers = useSwipeable({ 108 | onSwipedLeft: () => slide(NEXT), 109 | onSwipedRight: () => slide(PREV), 110 | swipeDuration: 500, 111 | preventScrollOnSwipe: true, 112 | trackMouse: true 113 | }); 114 | 115 | return ( 116 |
117 | 118 | 119 | {React.Children.map(props.children, (child, index) => ( 120 | 123 | {child} 124 | 125 | ))} 126 | 127 | 128 | 129 | slide(PREV)} float="left"> 130 | Prev 131 | 132 | slide(NEXT)} float="right"> 133 | Next 134 | 135 | 136 |
137 | ); 138 | }; 139 | ``` 140 | -------------------------------------------------------------------------------- /docs/docs/examples/simple-pattern.mdx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import SimplePattern from '@site/src/components/examples/SimplePattern' 4 | 5 | # Simple Pattern 6 | 7 | Below is an example implementation of a simple pattern which utilizes the hooks provided by `react-swipeable` within a TypeScript context. 8 | 9 | ## Simple Pattern Code Source 10 | 11 | You can see this full example as pure code within the [Pattern.tsx](https://github.com/FormidableLabs/react-swipeable/blob/main/examples/app/SimplePattern/pattern.tsx) file within the React-Swipeable repo directly. 12 | 13 | ## Simple Pattern Live Preview 14 | 15 | 16 | 17 | ## Simple Pattern Code Explained 18 | 19 | Import the hook directly from the `react-swipeable` library, along with the directions from the library, and the `SwipeEventData`. In our example, we built and imported a local set of UI components: you can utilize your own UI and styling, or use your favorite UI component library of choice. 20 | 21 | ```typescript 22 | import React, { FunctionComponent, ReactNode } from 'react'; 23 | import { useSwipeable, UP, DOWN, SwipeEventData } from 'react-swipeable'; 24 | import { 25 | Wrapper, 26 | CarouselContainer, 27 | CarouselSlot, 28 | PatternBox, 29 | PREV, 30 | NEXT, 31 | D 32 | } from '../components'; 33 | ``` 34 | 35 | In our example, we utilize SVGs for our `UpArrow` and `DownArrow` to give indications of when someone is successfully activating the pattern for user feedback, but know you can use whatever UI library of your choice, or stylize your own! 36 | 37 | ```typescript 38 | const UpArrow = ({active}: {active: boolean}) => ( 39 | 40 | 41 | 42 | 43 | 44 | ); 45 | 46 | const DownArrow = ({active}: {active: boolean}) => ( 47 | 48 | 49 | 50 | 51 | 52 | ); 53 | ``` 54 | 55 | Next, we set up our types for the `Directions`, `CarouselState`, and `CarouselAction`. 56 | 57 | ```typescript 58 | type Direction = typeof PREV | typeof NEXT; 59 | 60 | interface CarouselState { 61 | pos: number; 62 | sliding: boolean; 63 | dir: Direction; 64 | } 65 | 66 | type CarouselAction = 67 | | { type: Direction, numItems: number } 68 | | { type: 'stopSliding' }; 69 | ``` 70 | 71 | Below, we create a function called `getOrder`, which drives the position of each item in the carousel, and what order of position each will be displayed in the context of the carousel. Then, we set a `pattern` as an array of the pattern we want the user to follow to unlock the slide action. Finally here, we then set `getInitialState`, setting the position of the initial items, the `sliding`, as false, and the direction. 72 | 73 | ```typescript 74 | 75 | const getOrder = (index: number, pos: number, numItems: number) => { 76 | return index - pos < 0 ? numItems - Math.abs(index - pos) : index - pos; 77 | }; 78 | 79 | const pattern = [UP, DOWN, UP, DOWN]; 80 | 81 | const getInitialState = (numItems: number): CarouselState => ({ pos: numItems - 1, sliding: false, dir: NEXT }); 82 | 83 | ``` 84 | 85 | At the bottom of the file, we set up a reducer for controlling the action of the Carousel utilizing a switch to set the `CarouselState` logic. 86 | 87 | ```typescript 88 | function reducer(state: CarouselState, action: CarouselAction): CarouselState { 89 | switch (action.type) { 90 | case PREV: 91 | return { 92 | ...state, 93 | dir: PREV, 94 | sliding: true, 95 | pos: state.pos === 0 ? action.numItems - 1 : state.pos - 1 96 | }; 97 | case NEXT: 98 | return { 99 | ...state, 100 | dir: NEXT, 101 | sliding: true, 102 | pos: state.pos === action.numItems - 1 ? 0 : state.pos + 1 103 | }; 104 | case 'stopSliding': 105 | return { ...state, sliding: false }; 106 | default: 107 | return state; 108 | } 109 | } 110 | ``` 111 | 112 | Then, building upon the reducer logic, the `` is constructed. We hold the number of items within `numItems`, and utilize the reducer within the `React.useReducer` hook. 113 | 114 | By creating the `slide`, as a `const`, we can utilize that to call within `handleSwiped` as an action that is called upon the user successfully execution of the pattern. 115 | 116 | It may help to briefly look at the `handlers` for a moment, and how we utilize `useSwipeable`. Within this, with each `onSwiped`, we call `handleSwiped`. So for each swipe the user takes within the text box above the carousel, we execute `handleSwiped` and pass along the `eventData`. If the `eventData.dir` matches the pattern for this indexed (`pIdx`) item, and the direction indicated, then we `setPIdx` to a greater number. 117 | 118 | What does this do? Two things: it helps us know when the user successfully got to the end of the pattern, and activate the `slide` action, and it also controls the arrows activating the color within the `` to give feedback to the user that they were successful in activating the steps of the pattern! 119 | 120 | Two other important items to note: we utilized `onTouchStartOrOnMouseDown` to pass through `event.preventDefault()` as a callback, and used `touchEventOptions: {passive: false}` in case certain browsers ignored the `preventDefault()` callback bubbling up. 121 | 122 | From there, the rest of the UI of the component is built. The `` holds where the user swipes in order to interact with the Carousel itself, along with the arrows that give the feedback to the user that the pattern was successful. The `` holds the Carousel display and items. Our Simple Pattern is complete! 123 | 124 | ```typescript 125 | const Carousel: FunctionComponent<{children: ReactNode}> = (props) => { 126 | const numItems = React.Children.count(props.children); 127 | const [state, dispatch] = React.useReducer(reducer, getInitialState(numItems)); 128 | 129 | const slide = (dir: Direction) => { 130 | dispatch({ type: dir, numItems }); 131 | setTimeout(() => { 132 | dispatch({ type: 'stopSliding' }); 133 | }, 50); 134 | }; 135 | 136 | const [pIdx, setPIdx] = React.useState(0); 137 | 138 | const handleSwiped = (eventData: SwipeEventData) => { 139 | if (eventData.dir === pattern[pIdx]) { 140 | // user successfully got to the end of the pattern! 141 | if (pIdx + 1 === pattern.length) { 142 | setPIdx(pattern.length); 143 | slide(NEXT); 144 | setTimeout(() => { 145 | setPIdx(0); 146 | }, 50); 147 | } else { 148 | // user is cont. with the pattern 149 | setPIdx((i) => (i += 1)); 150 | } 151 | } else { 152 | // user got the next pattern step wrong, reset pattern 153 | setPIdx(0); 154 | } 155 | }; 156 | 157 | const handlers = useSwipeable({ 158 | onSwiped: handleSwiped, 159 | onTouchStartOrOnMouseDown: (({ event }) => event.preventDefault()), 160 | touchEventOptions: { passive: false }, 161 | preventScrollOnSwipe: true, 162 | trackMouse: true 163 | }); 164 | 165 | return ( 166 | <> 167 | 168 | Swipe the pattern below, within this box, to make the carousel go to the next 169 | slide 170 | {`\n`} 171 |

172 | Swipe: 173 | 0} /> 174 | 1} /> 175 | 2} /> 176 | 3} /> 177 |

178 |
179 |
180 | 181 | 182 | {React.Children.map(props.children, (child, index) => ( 183 | 187 | {child} 188 | 189 | ))} 190 | 191 | 192 |
193 | 194 | ); 195 | }; 196 | ``` 197 | 198 | -------------------------------------------------------------------------------- /docs/docs/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ### Version 7 Updates and migration 4 | 5 | If upgrading from v6 refer to the release notes and the [migration doc](./v7-migration). 6 | 7 | ### How can I add a swipe listener to the `document`? 8 | Example by [@merrywhether #180](https://github.com/FormidableLabs/react-swipeable/issues/180#issuecomment-649677983) 9 | 10 | ##### Example codesandbox with swipeable on document and nested swipe 11 | https://codesandbox.io/s/react-swipeable-document-swipe-example-1yvr2v 12 | 13 | ```js 14 | const { ref } = useSwipeable({ 15 | ... 16 | }) as { ref: RefCallback }; 17 | 18 | useEffect(() => { 19 | ref(document); 20 | // Clean up swipeable event listeners 21 | return () => ref({}); 22 | }); 23 | ``` 24 | **Note:** Issues can arise if you forget to clean up listeners - [#332](https://github.com/FormidableLabs/react-swipeable/issues/322) 25 | 26 | ### How to share `ref` from `useSwipeable`? 27 | 28 | **Example ref passthrough, [more details #189](https://github.com/FormidableLabs/react-swipeable/issues/189#issuecomment-656302682):** 29 | ```js 30 | const MyComponent = () => { 31 | const handlers = useSwipeable({ onSwiped: () => console.log('swiped') }) 32 | 33 | // setup ref for your usage 34 | const myRef = React.useRef(); 35 | 36 | const refPassthrough = (el) => { 37 | // call useSwipeable ref prop with el 38 | handlers.ref(el); 39 | 40 | // set myRef el so you can access it yourself 41 | myRef.current = el; 42 | } 43 | 44 | return (
45 | } 46 | ``` 47 | 48 | ### How to use `touch-action` to prevent scrolling? 49 | 50 | Sometimes you don't want the `body` of your page to scroll along with the user manipulating or swiping an item. Or you might want all of the internal event listeners to be passive and performant. 51 | 52 | You can prevent scrolling via [preventScrollOnSwipe](api#preventscrollonswipe-details), which calls `event.preventDefault()` during `onTouchMove`. **But** there may be a simpler, more effective solution, which has to do with a simple CSS property. 53 | 54 | `touch-action` is a CSS property that sets how an element's region can be manipulated by a touchscreen user. See the [documentation for `touch-action`](https://developer.mozilla.org/en-US/docs/Web/CSS/touch-action) to determine which property value to use for your particular use case. -------------------------------------------------------------------------------- /docs/docs/introduction.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | slug: / 4 | --- 5 | 6 | # Introduction 7 | 8 | Using react swipeable is easy! Follow the simple steps below: 9 | 10 | 1. Import `useSwipeable` hook from `react-swipeable` 11 | 2. Set the swipe handlers you care about 12 | 3. Spread the handlers onto an html tag to bind to the events 13 | 14 | ```jsx 15 | import { useSwipeable } from 'react-swipeable'; 16 | 17 | const handlers = useSwipeable({ 18 | onSwiped: (eventData) => console.log("User Swiped!", eventData), 19 | ...config, 20 | }); 21 | return
You can swipe here
; 22 | ``` 23 | 24 | Spread `handlers` onto the element you wish to track swipes on. 25 | 26 | import React from "react"; 27 | 28 | import SwipeDemo from '@site/src/components/examples/SwipeDemo' 29 | 30 | -------------------------------------------------------------------------------- /docs/docs/v7-migration.md: -------------------------------------------------------------------------------- 1 | # v7 migration guide 2 | 3 | ## :construction: Major Changes / Breaking Changes :construction: 4 | 5 | - we have dropped support for `es5` transpiled output 6 | - we target `es2015` for our transpilation now 7 | - `swipeable` utilizes object/array spread & const/let natively 8 | - `preventScrollOnSwipe` - "new" prop. Replaces `preventDefaultTouchmoveEvent` 9 | - same functionality but renamed to be more explicit on its intended use 10 | - **fixed bug** - where toggling this prop did not re-attach event listeners 11 | - **update** - we now **only** change the `passive` event listener option for `touchmove` depending on this prop 12 | - see notes in api docs for more details on [passive-listener](api#passive-listener-details) 13 | 14 | ### Typescript changes 15 | - Added a **ton** of comments to the types that should now show up in IDEs. 16 | 17 | ## Migrate Swipeable v6 to v7 18 | 19 | If you you're currently utilizing `preventDefaultTouchmoveEvent` you should be able to simply replace its usage with `preventScrollOnSwipe`. 20 | 21 | ```diff 22 | const handlers = useSwipeable({ 23 | - preventDefaultTouchmoveEvent: true, 24 | + preventScrollOnSwipe: true, 25 | }); 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/docusaurus.config.ts: -------------------------------------------------------------------------------- 1 | import { themes as prismThemes } from "prism-react-renderer"; 2 | import type { Config } from "@docusaurus/types"; 3 | import type * as Preset from "@docusaurus/preset-classic"; 4 | 5 | const config: Config = { 6 | title: "React Swipeable", 7 | tagline: "Customizable, fast, and lightweight React hook which provides all the information needed for your site to manage swipe interactions.", 8 | favicon: "img/nearform-icon.svg", 9 | url: "https://commerce.nearform.com/", 10 | baseUrl: "/open-source/react-swipeable", 11 | onBrokenLinks: "throw", 12 | onBrokenMarkdownLinks: "warn", 13 | i18n: { 14 | defaultLocale: "en", 15 | locales: ["en"], 16 | }, 17 | presets: [ 18 | [ 19 | "classic", 20 | { 21 | docs: { 22 | sidebarPath: "./sidebars.ts", 23 | editUrl: 24 | "https://github.com/FormidableLabs/react-swipeable/tree/main/", 25 | }, 26 | blog: false, 27 | theme: { 28 | customCss: "./src/css/custom.css", 29 | }, 30 | } satisfies Preset.Options, 31 | ], 32 | ], 33 | themes:[ 34 | [ 35 | require.resolve("@easyops-cn/docusaurus-search-local"), 36 | /** @type {import("@easyops-cn/docusaurus-search-local").PluginOptions} */ 37 | ({ 38 | hashed: true, 39 | }), 40 | ], 41 | ], 42 | plugins: [ 43 | async function myPlugin() { 44 | return { 45 | name: 'tailwind-plugin', 46 | configurePostCss(postcssOptions) { 47 | postcssOptions.plugins = [ 48 | require('postcss-import'), 49 | require('tailwindcss'), 50 | require('autoprefixer') 51 | ]; 52 | return postcssOptions 53 | }, 54 | }; 55 | }, 56 | ], 57 | themeConfig: { 58 | metadata: [ 59 | { name:"viewport", content:"width=device-width, initial-scale=1, maximum-scale=1"} 60 | ], 61 | docs: { 62 | sidebar: { 63 | hideable: true, 64 | }, 65 | }, 66 | navbar: { 67 | title: "React Swipeable", 68 | logo: { 69 | alt: "Nearform logo", 70 | src: "img/nearform-logo-white.svg", 71 | }, 72 | items: [ 73 | { 74 | type: "docSidebar", 75 | sidebarId: "sidebar", 76 | position: "left", 77 | label: "Documentation", 78 | }, 79 | { 80 | href: "https://github.com/FormidableLabs/react-swipeable", 81 | "aria-label": "GitHub Repository", 82 | className: "header-github-link", 83 | position: "right", 84 | }, 85 | ], 86 | }, 87 | footer: { 88 | logo: { 89 | alt: "Nearform logo", 90 | src: "img/nearform-logo-white.svg", 91 | href: "https://commerce.nearform.com", 92 | width: 100, 93 | height: 100, 94 | }, 95 | copyright: `Copyright © 2013-${new Date().getFullYear()} Nearform`, 96 | }, 97 | prism: { 98 | theme: prismThemes.github, 99 | darkTheme: prismThemes.dracula, 100 | }, 101 | } satisfies Preset.ThemeConfig, 102 | }; 103 | 104 | export default config; 105 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build --out-dir build/open-source/react-swipeable", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "3.1.1", 19 | "@docusaurus/preset-classic": "3.1.1", 20 | "@easyops-cn/docusaurus-search-local": "^0.40.1", 21 | "@mdx-js/react": "^3.0.0", 22 | "clsx": "^2.0.0", 23 | "formidable-oss-badges": "^1.3.2", 24 | "prism-react-renderer": "^2.3.0", 25 | "react": "^18.0.0", 26 | "react-dom": "^18.0.0", 27 | "react-swipeable": "^7.0.1", 28 | "styled-components": "^6.1.8" 29 | }, 30 | "devDependencies": { 31 | "@docusaurus/module-type-aliases": "3.1.1", 32 | "@docusaurus/tsconfig": "3.1.1", 33 | "@docusaurus/types": "3.1.1", 34 | "autoprefixer": "^10.4.19", 35 | "postcss": "^8.4.38", 36 | "tailwindcss": "^3.4.1", 37 | "typescript": "~5.2.2" 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.5%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 3 chrome version", 47 | "last 3 firefox version", 48 | "last 5 safari version" 49 | ] 50 | }, 51 | "engines": { 52 | "node": ">=18.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /docs/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type { SidebarsConfig } from "@docusaurus/plugin-content-docs"; 2 | 3 | const sidebars: SidebarsConfig = { 4 | sidebar: [{ type: "autogenerated", dirName: "." }], 5 | }; 6 | 7 | export default sidebars; 8 | -------------------------------------------------------------------------------- /docs/src/components/examples/FeatureTestConsole/SwipeableHook.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSwipeable } from 'react-swipeable'; 3 | 4 | function SwipeableHook(props: any) { 5 | const { children, className, style, ...rest } = props; 6 | const eventHandlers = useSwipeable(rest); 7 | return (
{children}
); 8 | } 9 | 10 | export default SwipeableHook; 11 | -------------------------------------------------------------------------------- /docs/src/components/examples/FeatureTestConsole/TableComponents.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function RowSimpleCheckbox({ value, name, displayText, onChange }: { value: any, name: string, displayText?: string, onChange: any }) { 4 | return ( 5 | 6 | {displayText || name}: 7 | 8 | onChange(name, e.target.checked)} /> 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /docs/src/components/examples/FeatureTestConsole/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { RowSimpleCheckbox } from "./TableComponents"; 3 | import SwipeableHook from "./SwipeableHook"; 4 | import { PatternBox } from "../components"; 5 | 6 | const DIRECTIONS = ["Left", "Right", "Up", "Down"]; 7 | 8 | const initialState = { 9 | swiping: false, 10 | swiped: false, 11 | tapped: false, 12 | swipingDirection: "", 13 | swipedDirection: "", 14 | }; 15 | const INFINITY = "Infinity"; 16 | const initialStateSwipeable = { 17 | delta: "10", 18 | preventScrollOnSwipe: false, 19 | trackMouse: false, 20 | trackTouch: true, 21 | rotationAngle: 0, 22 | swipeDuration: INFINITY, 23 | }; 24 | const initialStateApplied = { 25 | showOnSwipeds: false, 26 | onSwipingApplied: true, 27 | onSwipedApplied: true, 28 | onTapApplied: true, 29 | stopScrollCss: false, 30 | }; 31 | 32 | interface IState { 33 | swiping: boolean; 34 | swiped: boolean; 35 | tapped: boolean; 36 | swipingDirection: string; 37 | swipedDirection: string; 38 | delta: string; 39 | swipeDuration: string; 40 | preventScrollOnSwipe: boolean; 41 | trackMouse: boolean; 42 | trackTouch: boolean; 43 | rotationAngle: number | string; 44 | showOnSwipeds: boolean; 45 | onSwipingApplied: boolean; 46 | onSwipedApplied: boolean; 47 | onTapApplied: boolean; 48 | stopScrollCss: boolean; 49 | } 50 | 51 | export default class Main extends Component { 52 | constructor(props: any) { 53 | super(props); 54 | this.state = Object.assign( 55 | {}, 56 | initialState, 57 | initialStateSwipeable, 58 | initialStateApplied 59 | ); 60 | this.updateValue = this.updateValue.bind(this); 61 | } 62 | 63 | resetState(resetAll?: boolean) { 64 | if (resetAll) { 65 | this.setState( 66 | Object.assign( 67 | {}, 68 | initialState, 69 | initialStateSwipeable, 70 | initialStateApplied 71 | ) 72 | ); 73 | } else { 74 | this.setState(initialState); 75 | } 76 | } 77 | 78 | onSwiped(args: any) { 79 | console.log("swiped args: ", args); 80 | this.setState({ 81 | swiped: true, 82 | swiping: false, 83 | }); 84 | } 85 | 86 | onSwiping(args: any) { 87 | console.log("swiping args: ", args); 88 | 89 | this.setState({ 90 | swiping: true, 91 | swiped: false, 92 | swipingDirection: args.dir, 93 | }); 94 | } 95 | 96 | onTap(args: any) { 97 | console.log("tap args: ", args); 98 | 99 | this.setState({ 100 | swiping: false, 101 | swiped: false, 102 | tapped: true, 103 | }); 104 | } 105 | 106 | onSwipedDirection(direction: any) { 107 | this.setState({ 108 | swipedDirection: direction, 109 | }); 110 | } 111 | 112 | updateValue(type: string, value: any) { 113 | // @ts-ignore 114 | this.setState({ [type]: value }); 115 | } 116 | 117 | _renderAppliedDirRow(dir: string) { 118 | // @ts-ignore 119 | const checked = this.state[`onSwiped${dir}Applied`]; 120 | 121 | return ( 122 | 123 | 124 | 129 | this.updateValue(`onSwiped${dir}Applied`, e.target.checked) 130 | } 131 | /> 132 | 133 | {dir} 134 | 135 | ); 136 | } 137 | 138 | render() { 139 | const { 140 | swiping, 141 | swiped, 142 | tapped, 143 | swipingDirection, 144 | swipedDirection, 145 | delta, 146 | showOnSwipeds, 147 | onSwipingApplied, 148 | onSwipedApplied, 149 | onTapApplied, 150 | preventScrollOnSwipe, 151 | trackTouch, 152 | trackMouse, 153 | rotationAngle, 154 | swipeDuration, 155 | stopScrollCss, 156 | } = this.state; 157 | 158 | const isSwipeDurationInfinity = swipeDuration === INFINITY; 159 | const swipeDurationTextValue = isSwipeDurationInfinity 160 | ? INFINITY 161 | : swipeDuration; 162 | const isSwipeDurationNumber = isSwipeDurationInfinity 163 | ? Infinity 164 | : !(isNaN(swipeDuration as any) || swipeDuration === ""); 165 | 166 | const isDeltaNumber = !(isNaN(delta as any) || delta === ""); 167 | const isRotationAngleNumber = !( 168 | isNaN(rotationAngle as any) || rotationAngle === "" 169 | ); 170 | const deltaNum = isDeltaNumber ? +delta : 10; 171 | const rotationAngleNum = isRotationAngleNumber ? +rotationAngle : 0; 172 | 173 | const swipeableStyle = { 174 | fontSize: "0.75rem", 175 | touchAction: stopScrollCss ? "none" : "auto", 176 | }; 177 | 178 | const boundSwipes = getBoundSwipes(this); 179 | let swipeableDirProps: any = {}; 180 | if (onSwipingApplied) { 181 | // @ts-ignore 182 | swipeableDirProps.onSwiping = (...args: any) => this.onSwiping(...args); 183 | } 184 | if (onSwipedApplied) { 185 | // @ts-ignore 186 | swipeableDirProps.onSwiped = (...args: any) => this.onSwiped(...args); 187 | } 188 | if (onTapApplied) { 189 | // @ts-ignore 190 | swipeableDirProps.onTap = (...args: any) => this.onTap(...args); 191 | } 192 | 193 | return ( 194 |
195 |
196 | 208 |
this.resetState()} 210 | onMouseDown={() => this.resetState()} 211 | > 212 | 213 |
214 |

👆 Swipeable Box 👆

215 |
216 |
217 | Swipe within this box to test the useSwipeable{" "} 218 | hook. Open the browser console window to see the event 219 | details. 220 |
221 |
222 |
223 |
224 |

225 | See output below and check the console for 'onSwiping' and 226 | 'onSwiped' callback output(open dev tools) 227 |

228 | 229 | You can also 'toggle' the swiped props being applied to this 230 | container below. 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 252 | 253 | 254 | 255 | 256 | 266 | 267 | 268 | 269 | 270 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 300 | 301 | 302 | 303 | {showOnSwipeds && ( 304 | 305 | 323 | 324 | )} 325 | 326 | 329 | 340 | 341 | 342 | 345 | 358 | 359 | 360 | 366 | 379 | 380 | 385 | 390 | 395 | 396 |
Applied?ActionOutput
243 | 248 | this.updateValue("onSwipingApplied", e.target.checked) 249 | } 250 | /> 251 | onSwiping{swiping ? "True" : "False"}
257 | 262 | this.updateValue("onSwipedApplied", e.target.checked) 263 | } 264 | /> 265 | onSwiped{swiped ? "True" : "False"}
271 | 276 | this.updateValue("onTapApplied", e.target.checked) 277 | } 278 | /> 279 | onTap{tapped ? "True" : "False"}
onSwiping Direction{swipingDirection}
290 | ( 293 | e.preventDefault(), 294 | this.updateValue("showOnSwipeds", !showOnSwipeds) 295 | )} 296 | > 297 | {showOnSwipeds ? "↑ Hide ↑" : "↓ Show ↓"} 298 | 299 | onSwiped Direction{swipedDirection}
306 | 307 | 308 | 309 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | {DIRECTIONS.map(this._renderAppliedDirRow.bind(this))} 320 | 321 |
310 | onSwiped 311 |
Applied?Direction
322 |
327 | delta: 328 | 330 | this.updateValue("delta", getVal(e))} 337 | value={delta} 338 | /> 339 |
343 | rotationAngle: 344 | 346 | 353 | this.updateValue("rotationAngle", getVal(e)) 354 | } 355 | value={rotationAngle} 356 | /> 357 |
361 | swipeDuration: 362 |
363 | (ms | Infinity) 364 |
365 |
367 | 374 | this.updateValue("swipeDuration", getVal(e)) 375 | } 376 | value={swipeDurationTextValue} 377 | /> 378 |
397 | 398 | 399 | 405 | 406 |
407 | 414 |
415 |
416 | ); 417 | } 418 | } 419 | 420 | function getBoundSwipes(component: any) { 421 | let boundSwipes = {}; 422 | DIRECTIONS.forEach((dir) => { 423 | if (component.state[`onSwiped${dir}Applied`]) { 424 | // @ts-ignore 425 | boundSwipes[`onSwiped${dir}`] = component.onSwipedDirection.bind( 426 | component, 427 | dir 428 | ); 429 | } 430 | }); 431 | return boundSwipes; 432 | } 433 | 434 | function getVal(e: any) { 435 | return e.target.value; 436 | } 437 | -------------------------------------------------------------------------------- /docs/src/components/examples/SimpleCarousel/Carousel.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, ReactNode } from 'react'; 2 | import { useSwipeable } from 'react-swipeable'; 3 | import { 4 | Wrapper, 5 | CarouselContainer, 6 | CarouselSlot, 7 | SlideButtonContainer, 8 | SlideButton, 9 | PREV, 10 | NEXT 11 | } from '../components'; 12 | 13 | type Direction = typeof PREV | typeof NEXT; 14 | 15 | interface CarouselState { 16 | pos: number; 17 | sliding: boolean; 18 | dir: Direction; 19 | } 20 | 21 | type CarouselAction = 22 | | { type: Direction, numItems: number } 23 | | { type: 'stopSliding' }; 24 | 25 | const getOrder = (index: number, pos: number, numItems: number) => { 26 | return index - pos < 0 ? numItems - Math.abs(index - pos) : index - pos; 27 | }; 28 | 29 | const getInitialState = (numItems: number): CarouselState => ({ pos: numItems - 1, sliding: false, dir: NEXT }); 30 | 31 | const Carousel: FunctionComponent<{children: ReactNode}> = (props) => { 32 | const numItems = React.Children.count(props.children); 33 | const [state, dispatch] = React.useReducer(reducer, getInitialState(numItems)); 34 | 35 | const slide = (dir: Direction) => { 36 | dispatch({ type: dir, numItems }); 37 | setTimeout(() => { 38 | dispatch({ type: 'stopSliding' }); 39 | }, 50); 40 | }; 41 | 42 | const handlers = useSwipeable({ 43 | onSwipedLeft: () => slide(NEXT), 44 | onSwipedRight: () => slide(PREV), 45 | swipeDuration: 500, 46 | preventScrollOnSwipe: true, 47 | trackMouse: true 48 | }); 49 | 50 | return ( 51 |
52 | 53 | 54 | {React.Children.map(props.children, (child, index) => ( 55 | 58 | {child} 59 | 60 | ))} 61 | 62 | 63 | 64 | slide(PREV)} float="left"> 65 | Prev 66 | 67 | slide(NEXT)} float="right"> 68 | Next 69 | 70 | 71 |
72 | ); 73 | }; 74 | 75 | function reducer(state: CarouselState, action: CarouselAction): CarouselState { 76 | switch (action.type) { 77 | case PREV: 78 | return { 79 | ...state, 80 | dir: PREV, 81 | sliding: true, 82 | pos: state.pos === 0 ? action.numItems - 1 : state.pos - 1 83 | }; 84 | case NEXT: 85 | return { 86 | ...state, 87 | dir: NEXT, 88 | sliding: true, 89 | pos: state.pos === action.numItems - 1 ? 0 : state.pos + 1 90 | }; 91 | case 'stopSliding': 92 | return { ...state, sliding: false }; 93 | default: 94 | return state; 95 | } 96 | } 97 | 98 | export default Carousel; 99 | -------------------------------------------------------------------------------- /docs/src/components/examples/SimpleCarousel/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Item } from "../components"; 3 | import Carousel from "./Carousel"; 4 | import { Product1, Product2, Product3, Product4, Product5 } from "../images"; 5 | 6 | // Carousel originally copied from: 7 | // https://medium.com/@incubation.ff/build-your-own-css-carousel-in-react-part-one-86f71f6670ca 8 | 9 | function SimpleCarousel() { 10 | return ( 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | ); 21 | } 22 | 23 | export default SimpleCarousel; 24 | -------------------------------------------------------------------------------- /docs/src/components/examples/SimplePattern/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Item } from "../components"; 3 | import Carousel from "./pattern"; 4 | import { Product1, Product2, Product3, Product4, Product5 } from "../images"; 5 | // Carousel originally copied from: 6 | // https://medium.com/@incubation.ff/build-your-own-css-carousel-in-react-part-one-86f71f6670ca 7 | 8 | function SimplePattern() { 9 | return ( 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | ); 20 | } 21 | 22 | export default SimplePattern; 23 | -------------------------------------------------------------------------------- /docs/src/components/examples/SimplePattern/pattern.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, ReactNode } from "react"; 2 | import { useSwipeable, UP, DOWN, SwipeEventData } from "react-swipeable"; 3 | import { 4 | Wrapper, 5 | CarouselContainer, 6 | CarouselSlot, 7 | PatternBox, 8 | PREV, 9 | NEXT, 10 | D, 11 | } from "../components"; 12 | 13 | const UpArrow = ({ active }: { active: boolean }) => ( 14 | 15 | 16 | 20 | 21 | 22 | ); 23 | 24 | const DownArrow = ({ active }: { active: boolean }) => ( 25 | 26 | 27 | 31 | 32 | 33 | ); 34 | 35 | type Direction = typeof PREV | typeof NEXT; 36 | 37 | interface CarouselState { 38 | pos: number; 39 | sliding: boolean; 40 | dir: Direction; 41 | } 42 | 43 | type CarouselAction = 44 | | { type: Direction; numItems: number } 45 | | { type: "stopSliding" }; 46 | 47 | const getOrder = (index: number, pos: number, numItems: number) => { 48 | return index - pos < 0 ? numItems - Math.abs(index - pos) : index - pos; 49 | }; 50 | 51 | const pattern = [UP, DOWN, UP, DOWN]; 52 | 53 | const getInitialState = (numItems: number): CarouselState => ({ 54 | pos: numItems - 1, 55 | sliding: false, 56 | dir: NEXT, 57 | }); 58 | 59 | const Carousel: FunctionComponent<{ children: ReactNode }> = (props) => { 60 | const numItems = React.Children.count(props.children); 61 | const [state, dispatch] = React.useReducer( 62 | reducer, 63 | getInitialState(numItems) 64 | ); 65 | 66 | const slide = (dir: Direction) => { 67 | dispatch({ type: dir, numItems }); 68 | setTimeout(() => { 69 | dispatch({ type: "stopSliding" }); 70 | }, 50); 71 | }; 72 | 73 | const [pIdx, setPIdx] = React.useState(0); 74 | 75 | const handleSwiped = (eventData: SwipeEventData) => { 76 | if (eventData.dir === pattern[pIdx]) { 77 | // user successfully got to the end of the pattern! 78 | if (pIdx + 1 === pattern.length) { 79 | setPIdx(pattern.length); 80 | slide(NEXT); 81 | setTimeout(() => { 82 | setPIdx(0); 83 | }, 50); 84 | } else { 85 | // user is cont. with the pattern 86 | setPIdx((i) => (i += 1)); 87 | } 88 | } else { 89 | // user got the next pattern step wrong, reset pattern 90 | setPIdx(0); 91 | } 92 | }; 93 | 94 | const handlers = useSwipeable({ 95 | onSwiped: handleSwiped, 96 | onTouchStartOrOnMouseDown: ({ event }) => event.preventDefault(), 97 | touchEventOptions: { passive: false }, 98 | preventScrollOnSwipe: true, 99 | trackMouse: true, 100 | }); 101 | 102 | return ( 103 | <> 104 | 105 | Within this text area container, swipe the pattern seen below to make 106 | the carousel navigate to the next slide. 107 | {`\n`} 108 |

109 | Swipe: 110 | 111 | 0} /> 112 | 113 | 114 | 1} /> 115 | 116 | 117 | 2} /> 118 | 119 | 120 | 3} /> 121 | 122 |

123 |
124 |
125 | 126 | 127 | {React.Children.map(props.children, (child, index) => ( 128 | 132 | {child} 133 | 134 | ))} 135 | 136 | 137 |
138 | 139 | ); 140 | }; 141 | 142 | function reducer(state: CarouselState, action: CarouselAction): CarouselState { 143 | switch (action.type) { 144 | case PREV: 145 | return { 146 | ...state, 147 | dir: PREV, 148 | sliding: true, 149 | pos: state.pos === 0 ? action.numItems - 1 : state.pos - 1, 150 | }; 151 | case NEXT: 152 | return { 153 | ...state, 154 | dir: NEXT, 155 | sliding: true, 156 | pos: state.pos === action.numItems - 1 ? 0 : state.pos + 1, 157 | }; 158 | case "stopSliding": 159 | return { ...state, sliding: false }; 160 | default: 161 | return state; 162 | } 163 | } 164 | 165 | export default Carousel; 166 | -------------------------------------------------------------------------------- /docs/src/components/examples/SwipeDemo/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { PatternBox } from "../components"; 3 | import { useSwipeable, SwipeEventData } from "react-swipeable"; 4 | import { Divider } from "../../landing/divider"; 5 | 6 | function SwipeDemo({ showDivider }: { showDivider?: boolean }) { 7 | const [swipeText, setSwipeText] = useState("🔮 Which way did you swipe? 🔮"); 8 | 9 | const changeText = (text) => { 10 | setSwipeText(text); 11 | }; 12 | const handleSwiped = (eventData: SwipeEventData) => { 13 | const baseText = "🧙 You swiped"; 14 | switch (eventData.dir) { 15 | case "Down": 16 | changeText(`${baseText} ⬇️!`); 17 | break; 18 | case "Left": 19 | changeText(`${baseText} ⬅️!`); 20 | break; 21 | case "Right": 22 | changeText(`${baseText} ➡️!`); 23 | break; 24 | case "Up": 25 | changeText(`${baseText} ⬆️!`); 26 | default: 27 | break; 28 | } 29 | console.log(`${baseText} ${eventData.dir}. Event data 👇`); 30 | console.log(eventData); 31 | }; 32 | 33 | const handlers = useSwipeable({ 34 | onSwiped: handleSwiped, 35 | onTouchStartOrOnMouseDown: ({ event }) => event.preventDefault(), 36 | touchEventOptions: { passive: false }, 37 | preventScrollOnSwipe: true, 38 | trackMouse: true, 39 | }); 40 | return ( 41 | <> 42 |
43 | {showDivider && } 44 |

Swipe Demo

45 | 46 |
47 |

👆 Swipeable Box 👆

48 |
49 |
50 | Swipe within this box to test the useSwipeable hook. 51 | Open the browser console window to see the event details. 52 |
53 | 54 |
{swipeText}
55 |
56 |
57 | 58 | ); 59 | } 60 | 61 | export default SwipeDemo; 62 | -------------------------------------------------------------------------------- /docs/src/components/examples/components.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const NEXT = "NEXT"; 4 | export const PREV = "PREV"; 5 | 6 | export const Item = styled.img<{ src: string }>` 7 | src: ${(props) => `url(${props.src})`} 8 | user-drag: none; 9 | -webkit-user-drag: none; 10 | user-select: none; 11 | -moz-user-select: none; 12 | -webkit-user-select: none; 13 | -ms-user-select: none; 14 | `; 15 | 16 | export const CarouselContainer = styled.div<{ sliding: boolean }>` 17 | display: flex; 18 | transition: ${(props) => (props.sliding ? "none" : "transform 1s ease")}; 19 | transform: ${(props) => { 20 | if (!props.sliding) return "translateX(calc(-80% - 20px))"; 21 | if (props.dir === PREV) return "translateX(calc(2 * (-80% - 20px)))"; 22 | return "translateX(0%)"; 23 | }}; 24 | `; 25 | 26 | export const Wrapper = styled.div` 27 | width: 100%; 28 | overflow: hidden; 29 | box-shadow: 5px 5px 20px 7px rgba(168, 168, 168, 1); 30 | `; 31 | 32 | export const CarouselSlot = styled.div<{ order: number }>` 33 | flex: 1 0 80%; 34 | margin-right: 5px; 35 | order: ${(props) => props.order}; 36 | `; 37 | 38 | export const SlideButtonContainer = styled.div` 39 | display: flex; 40 | justify-content: space-between; 41 | padding-bottom: 10px; 42 | `; 43 | 44 | export const SlideButton = styled.button<{ float: "left" | "right" }>` 45 | color: #ffffff; 46 | font-family: Open Sans; 47 | font-size: 16px; 48 | font-weight: 100; 49 | padding: 10px; 50 | background-color: #f66f3e; 51 | border: 1px solid white; 52 | text-decoration: none; 53 | display: inline-block; 54 | cursor: pointer; 55 | margin-top: 20px; 56 | text-decoration: none; 57 | 58 | &:active { 59 | position: relative; 60 | top: 1px; 61 | } 62 | &:focus { 63 | outline: 0; 64 | } 65 | `; 66 | 67 | export const PatternBox = styled.div` 68 | padding: 10px; 69 | border: 1px solid black; 70 | margin: 10px auto 20px auto; 71 | text-align: center; 72 | `; 73 | 74 | export const D = styled.span` 75 | padding: 3px; 76 | `; 77 | -------------------------------------------------------------------------------- /docs/src/components/examples/images.tsx: -------------------------------------------------------------------------------- 1 | import Product1 from "@site/static/img/product-1.jpg"; 2 | import Product2 from "@site/static/img/product-2.jpg"; 3 | import Product3 from "@site/static/img/product-3.jpg"; 4 | import Product4 from "@site/static/img/product-4.jpg"; 5 | import Product5 from "@site/static/img/product-5.jpg"; 6 | 7 | export { Product1, Product2, Product3, Product4, Product5 }; 8 | -------------------------------------------------------------------------------- /docs/src/components/landing/divider.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const Divider = () => { 4 | return
; 5 | }; 6 | -------------------------------------------------------------------------------- /docs/src/components/landing/landing-banner.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NFLinkButton } from "./nf-link-button"; 3 | import { Divider } from "./divider"; 4 | 5 | export const LandingBanner = ({ 6 | body, 7 | cta, 8 | heading, 9 | showDivider, 10 | }: { 11 | body: string; 12 | cta: { link: string; text: string }; 13 | heading: string; 14 | showDivider?: boolean; 15 | }) => ( 16 |
17 | {showDivider && } 18 | 19 |

{heading}

20 |

{body}

21 | 22 |
23 | ); 24 | -------------------------------------------------------------------------------- /docs/src/components/landing/landing-featured-projects.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FeaturedBadge } from "formidable-oss-badges"; 3 | import { NFLinkButton } from "./nf-link-button"; 4 | import { Divider } from "./divider"; 5 | 6 | type featuredProject = 7 | | "renature" 8 | | "spectacle" 9 | | "urql" 10 | | "victory" 11 | | "nuka" 12 | | "owl" 13 | | "groqd" 14 | | "envy" 15 | | "figlog"; 16 | 17 | export const LandingFeaturedProjects = ({ 18 | heading, 19 | projects, 20 | showDivider, 21 | }: { 22 | heading: string; 23 | projects: { 24 | name: featuredProject; 25 | link: string; 26 | description: string; 27 | title?: string; 28 | }[]; 29 | showDivider?: boolean; 30 | }) => ( 31 |
32 | {showDivider && } 33 |

{heading}

34 |
35 | {projects.map(({ name, link, description, title }) => ( 36 | 41 | 42 | 43 | 44 | {title || name} 45 | 46 | {description} 47 | 48 | 49 | ))} 50 |
51 | 52 |
53 | 57 |
58 |
59 | ); 60 | -------------------------------------------------------------------------------- /docs/src/components/landing/landing-features.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Divider } from "./divider"; 3 | 4 | export const LandingFeatures = ({ 5 | heading, 6 | list, 7 | showDivider, 8 | }: { 9 | heading: string; 10 | list: { 11 | imgSrc: string; 12 | alt: string; 13 | title: string; 14 | body?: string; 15 | html?: { __html: string }; 16 | }[]; 17 | showDivider?: boolean; 18 | }) => ( 19 |
20 | {showDivider && } 21 |

{heading}

22 |
    23 | {list.map(({ alt, body, imgSrc, title, html }, i) => ( 24 |
  • 28 | {alt} 29 | {title} 30 | 34 | {body} 35 | 36 |
  • 37 | ))} 38 |
39 |
40 | ); 41 | -------------------------------------------------------------------------------- /docs/src/components/landing/landing-hero.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { ProjectBadge } from "formidable-oss-badges"; 3 | 4 | export const LandingHero = ({ 5 | body, 6 | copyText, 7 | heading, 8 | navItems, 9 | }: { 10 | body: string; 11 | copyText: string; 12 | heading: string; 13 | navItems: { link: string; title: string }[]; 14 | }) => { 15 | const [buttonText, setButtonText] = useState("Copy"); 16 | 17 | const changeText = (text) => { 18 | setButtonText(text); 19 | }; 20 | 21 | return ( 22 |
23 |
24 |
25 |
26 | 32 |
33 |
34 |

35 | {heading} 36 |

37 |

{body}

38 |
39 | 53 |
54 | 65 |
66 |
67 |
68 |
69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /docs/src/components/landing/landing-images.tsx: -------------------------------------------------------------------------------- 1 | import feature1 from "@site/static/img/feature-1.png"; 2 | import feature2 from "@site/static/img/feature-2.png"; 3 | import feature3 from "@site/static/img/feature-3.png"; 4 | 5 | export { feature1, feature2, feature3 }; 6 | -------------------------------------------------------------------------------- /docs/src/components/landing/nf-link-button.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface ButtonProps { 4 | text: string; 5 | link: string; 6 | screenReaderLabel?: string; 7 | } 8 | 9 | export const NFLinkButton = ({ text, link }: ButtonProps) => { 10 | return ( 11 | 15 | {text} 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /docs/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | @tailwind base; 7 | @tailwind components; 8 | @tailwind utilities; 9 | 10 | body { 11 | scroll-behavior: smooth; 12 | text-rendering: optimizeSpeed; 13 | } 14 | 15 | @font-face { 16 | font-family: "Inter"; 17 | src: url("/font/InterRegular.woff2") format("woff2"); 18 | font-weight: 400; 19 | font-style: normal; 20 | font-display: swap; 21 | } 22 | 23 | @font-face { 24 | font-family: "Inter"; 25 | src: url("/font/InterMedium.woff2") format("woff2"); 26 | font-weight: 500; 27 | font-style: normal; 28 | font-display: swap; 29 | } 30 | 31 | @font-face { 32 | font-family: "Inter"; 33 | src: url("/font/InterBold.woff2") format("woff2"); 34 | font-weight: 700; 35 | font-style: normal; 36 | font-display: swap; 37 | } 38 | 39 | .hero-pattern { 40 | background-image: url("/img/hero-pattern.png"); 41 | } 42 | 43 | :root { 44 | --ifm-color-primary: #5abdee; 45 | --ifm-color-primary-dark: #3cb1eb; 46 | --ifm-color-primary-darker: #2dabe9; 47 | --ifm-color-primary-darkest: #1592d0; 48 | --ifm-color-primary-light: #78c9f1; 49 | --ifm-color-primary-lighter: #87cff3; 50 | --ifm-color-primary-lightest: #b3e1f7; 51 | --ifm-code-font-size: 95%; 52 | --ifm-list-item-margin: 0; 53 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 54 | --ifm-navbar-background-color: #000e38; 55 | --ifm-navbar-link-color: #ffffff; 56 | --ifm-footer-background-color: #000e38; 57 | --ifm-footer-padding-vertical: 1rem; 58 | } 59 | 60 | [data-theme="dark"] { 61 | --ifm-color-primary: #5abdee; 62 | --ifm-color-primary-dark: #3cb1eb; 63 | --ifm-color-primary-darker: #2dabe9; 64 | --ifm-color-primary-darkest: #1592d0; 65 | --ifm-color-primary-light: #78c9f1; 66 | --ifm-color-primary-lighter: #87cff3; 67 | --ifm-color-primary-lightest: #b3e1f7; 68 | --ifm-list-item-margin: 0; 69 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 70 | --ifm-navbar-background-color: #242526; 71 | --ifm-footer-background-color: #242526; 72 | } 73 | 74 | /* Nav */ 75 | .header-github-link::before { 76 | content: ""; 77 | width: 24px; 78 | height: 24px; 79 | display: flex; 80 | background: url("data:image/svg+xml;charset=utf-8,%3Csvg fill='white' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") 81 | no-repeat; 82 | } 83 | 84 | .navbar__inner button svg { 85 | color: white; 86 | } 87 | 88 | /* Custom Footer */ 89 | .footer__bottom.text--center { 90 | display: flex; 91 | justify-content: space-between; 92 | align-items: center; 93 | } 94 | 95 | .footer__bottom.text--center a { 96 | opacity: 1 !important; 97 | } 98 | 99 | .footer__copyright { 100 | color: white; 101 | } 102 | -------------------------------------------------------------------------------- /docs/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; 3 | import Layout from "@theme/Layout"; 4 | 5 | import { LandingHero } from "../components/landing/landing-hero"; 6 | import { LandingBanner } from "../components/landing/landing-banner"; 7 | import { LandingFeaturedProjects } from "../components/landing/landing-featured-projects"; 8 | import { LandingFeatures } from "../components/landing/landing-features"; 9 | import SwipeDemo from "../components/examples/SwipeDemo"; 10 | import { 11 | feature1, 12 | feature2, 13 | feature3, 14 | } from "../components/landing/landing-images"; 15 | 16 | export default function Home(): JSX.Element { 17 | const { siteConfig } = useDocusaurusContext(); 18 | return ( 19 | 20 |
21 | 34 |
35 | useSwipeable hook provides you with all the information you need to know about a user's swipe behavior.", 45 | }, 46 | }, 47 | { 48 | imgSrc: feature2, 49 | alt: "flexible", 50 | title: "Flexible", 51 | html: { 52 | __html: 53 | "useSwipeable is minimal, versatile and flexible. It can be attached to any HTML element, which allows for unlimited possibilities in component design.", 54 | }, 55 | }, 56 | { 57 | imgSrc: feature3, 58 | alt: "powered by react", 59 | title: "Powered by React", 60 | body: 61 | "Swipeable leverages the power of React's declarative and component-based architecture for immersive swipe interactions.", 62 | }, 63 | ]} 64 | /> 65 | 66 | 72 | 101 |
102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/react-swipeable/d12491005fc1f68788c9e3b4ed2297d5ac2c5ae9/docs/static/.nojekyll -------------------------------------------------------------------------------- /docs/static/font/InterBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/react-swipeable/d12491005fc1f68788c9e3b4ed2297d5ac2c5ae9/docs/static/font/InterBold.woff2 -------------------------------------------------------------------------------- /docs/static/font/InterMedium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/react-swipeable/d12491005fc1f68788c9e3b4ed2297d5ac2c5ae9/docs/static/font/InterMedium.woff2 -------------------------------------------------------------------------------- /docs/static/font/InterRegular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/react-swipeable/d12491005fc1f68788c9e3b4ed2297d5ac2c5ae9/docs/static/font/InterRegular.woff2 -------------------------------------------------------------------------------- /docs/static/img/feature-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/react-swipeable/d12491005fc1f68788c9e3b4ed2297d5ac2c5ae9/docs/static/img/feature-1.png -------------------------------------------------------------------------------- /docs/static/img/feature-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/react-swipeable/d12491005fc1f68788c9e3b4ed2297d5ac2c5ae9/docs/static/img/feature-2.png -------------------------------------------------------------------------------- /docs/static/img/feature-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/react-swipeable/d12491005fc1f68788c9e3b4ed2297d5ac2c5ae9/docs/static/img/feature-3.png -------------------------------------------------------------------------------- /docs/static/img/hero-pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/react-swipeable/d12491005fc1f68788c9e3b4ed2297d5ac2c5ae9/docs/static/img/hero-pattern.png -------------------------------------------------------------------------------- /docs/static/img/nearform-icon-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/static/img/nearform-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/static/img/nearform-logo-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/static/img/nearform-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/static/img/product-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/react-swipeable/d12491005fc1f68788c9e3b4ed2297d5ac2c5ae9/docs/static/img/product-1.jpg -------------------------------------------------------------------------------- /docs/static/img/product-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/react-swipeable/d12491005fc1f68788c9e3b4ed2297d5ac2c5ae9/docs/static/img/product-2.jpg -------------------------------------------------------------------------------- /docs/static/img/product-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/react-swipeable/d12491005fc1f68788c9e3b4ed2297d5ac2c5ae9/docs/static/img/product-3.jpg -------------------------------------------------------------------------------- /docs/static/img/product-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/react-swipeable/d12491005fc1f68788c9e3b4ed2297d5ac2c5ae9/docs/static/img/product-4.jpg -------------------------------------------------------------------------------- /docs/static/img/product-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/react-swipeable/d12491005fc1f68788c9e3b4ed2297d5ac2c5ae9/docs/static/img/product-5.jpg -------------------------------------------------------------------------------- /docs/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | 3 | const NearFormColors = { 4 | White: "hsla(0, 0%, 100%, 1)", 5 | Black: "hsla(0, 0%, 0%, 1)", 6 | Green: "hsla(163, 100%, 45%, 1)", 7 | Purple: "hsla(260, 100%, 70%, 1)", 8 | LightPurple: "hsla(262, 100%, 90%, 1)", 9 | Blue: "hsla(218, 100%, 64%, 1)", 10 | LightBlue: "hsla(217, 100%, 92%, 1)", 11 | Grey: "hsla(0, 0%, 85%, 1)", 12 | LightGrey: "#F4F8FA", 13 | DeepGrey: "hsla(240, 8%, 29%, 1)", 14 | Navy: "hsla(205, 78%, 21%, 1)", 15 | LightNavy: "hsla(222, 25%, 43%, 1)", 16 | DeepNavy: "hsla(225, 100%, 11%, 1)", 17 | }; 18 | 19 | module.exports = { 20 | corePlugins: { 21 | preflight: false, // disable Tailwind's reset 22 | }, 23 | content: ["./src/**/*.{js,jsx,ts,tsx}", "../docs/**/*.mdx"], 24 | darkMode: ["class", '[data-theme="dark"]'], 25 | theme: { 26 | extend: { 27 | colors: { 28 | transparent: "transparent", 29 | white: NearFormColors.White, 30 | black: NearFormColors.Black, 31 | grayscale: { 32 | 100: NearFormColors.White, 33 | 200: NearFormColors.LightGrey, 34 | 300: NearFormColors.Grey, 35 | 400: NearFormColors.DeepGrey, 36 | 500: NearFormColors.Black, 37 | 800: "#888888", 38 | }, 39 | "theme-1": NearFormColors.Green, 40 | "theme-2": NearFormColors.DeepNavy, 41 | "theme-3": NearFormColors.DeepNavy, 42 | "theme-4": NearFormColors.White, 43 | "header-bg": NearFormColors.White, 44 | "header-fg": NearFormColors.DeepNavy, 45 | "button-bg": NearFormColors.Green, 46 | "button-fg": NearFormColors.DeepNavy, 47 | "button-bg-hover": NearFormColors.White, 48 | "button-fg-hover": NearFormColors.DeepNavy, 49 | "button-border": NearFormColors.Green, 50 | error: "#FF0000", 51 | }, 52 | width: { 53 | prose: "90ch", 54 | }, 55 | fontFamily: { 56 | sans: ["Inter, Helvetica, Arial, sans-serif"], 57 | }, 58 | }, 59 | }, 60 | }; 61 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@docusaurus/tsconfig", 3 | "compilerOptions": { 4 | "baseUrl": "." 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | react-swipeable examples 2 | ================ 3 | 4 | This is the code that generates the example app - http://stack.formidable.com/react-swipeable/. 5 | 6 | You can play with the examples on [codesandbox](https://codesandbox.io/s/github/FormidableLabs/react-swipeable/tree/main/examples). 7 | 8 | Or you can run `yarn && yarn start` inside of this folder to run the examples locally. 9 | -------------------------------------------------------------------------------- /examples/app/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import FeatureTestConsole from './FeatureTestConsole'; 3 | import SimpleCarousel from './SimpleCarousel'; 4 | import SimplePattern from './SimplePattern'; 5 | import { Paper } from './components'; 6 | 7 | export default function App({version}: {version: any}) { 8 | return ( 9 |
10 |
11 |

12 | react-swipeable  13 | 17 | v{version} 18 | 19 |

20 | 21 |
22 |
Examples:
23 |
💻 Feature testing with console log ⇨
24 |
🖼 Image Carousel ⇨
25 |
👉 Swipe pattern ⇨
26 |
27 |
28 | 29 | 30 | 31 | 32 |
33 | 34 | 35 | 36 | 37 |
38 | 39 | 40 | 41 | 42 |
43 | 44 |

45 | Thanks for checking out the examples! Let us know if you discover anything or have thoughts on improvements, and  46 | submit an issue or PR! 47 |

48 |
49 |
50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /examples/app/FeatureTestConsole/SwipeableHook.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSwipeable } from 'react-swipeable'; 3 | 4 | function SwipeableHook(props: any) { 5 | const { children, className, style, ...rest } = props; 6 | const eventHandlers = useSwipeable(rest); 7 | return (
{children}
); 8 | } 9 | 10 | export default SwipeableHook; 11 | -------------------------------------------------------------------------------- /examples/app/FeatureTestConsole/TableComponents.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function RowSimpleCheckbox({ value, name, displayText, onChange }: { value: any, name: string, displayText?: string, onChange: any }) { 4 | return ( 5 | 6 | {displayText || name}: 7 | 8 | onChange(name, e.target.checked)} /> 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /examples/app/FeatureTestConsole/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import { RowSimpleCheckbox } from './TableComponents'; 3 | import SwipeableHook from './SwipeableHook'; 4 | 5 | const DIRECTIONS = ['Left', 'Right', 'Up', 'Down']; 6 | 7 | const initialState = { 8 | swiping: false, 9 | swiped: false, 10 | tapped: false, 11 | swipingDirection: '', 12 | swipedDirection: '', 13 | }; 14 | const INFINITY = 'Infinity'; 15 | const initialStateSwipeable = { 16 | delta: '10', 17 | preventScrollOnSwipe: false, 18 | trackMouse: false, 19 | trackTouch: true, 20 | rotationAngle: 0, 21 | swipeDuration: INFINITY, 22 | }; 23 | const initialStateApplied = { 24 | showOnSwipeds: false, 25 | onSwipingApplied: true, 26 | onSwipedApplied: true, 27 | onTapApplied: true, 28 | stopScrollCss: false, 29 | }; 30 | 31 | interface IState { 32 | swiping: boolean; 33 | swiped: boolean; 34 | tapped: boolean; 35 | swipingDirection: string; 36 | swipedDirection: string; 37 | delta: string; 38 | swipeDuration: string; 39 | preventScrollOnSwipe: boolean; 40 | trackMouse: boolean; 41 | trackTouch: boolean; 42 | rotationAngle: number | string; 43 | showOnSwipeds: boolean; 44 | onSwipingApplied: boolean; 45 | onSwipedApplied: boolean; 46 | onTapApplied: boolean; 47 | stopScrollCss: boolean; 48 | } 49 | 50 | export default class Main extends Component { 51 | constructor(props: any) { 52 | super(props); 53 | this.state = Object.assign({}, initialState, initialStateSwipeable, initialStateApplied); 54 | this.updateValue = this.updateValue.bind(this); 55 | } 56 | 57 | resetState(resetAll?: boolean) { 58 | if (resetAll) { 59 | this.setState(Object.assign({}, initialState, initialStateSwipeable, initialStateApplied)); 60 | } else { 61 | this.setState(initialState); 62 | } 63 | } 64 | 65 | onSwiped(args: any) { 66 | console.log('swiped args: ', args) 67 | this.setState({ 68 | swiped: true, 69 | swiping: false, 70 | }); 71 | } 72 | 73 | onSwiping(args: any) { 74 | console.log('swiping args: ', args) 75 | 76 | this.setState({ 77 | swiping: true, 78 | swiped: false, 79 | swipingDirection: args.dir, 80 | }); 81 | } 82 | 83 | onTap(args: any) { 84 | console.log('tap args: ', args) 85 | 86 | this.setState({ 87 | swiping: false, 88 | swiped: false, 89 | tapped: true 90 | }) 91 | } 92 | 93 | onSwipedDirection(direction: any) { 94 | this.setState({ 95 | swipedDirection: direction, 96 | }); 97 | } 98 | 99 | updateValue(type: string, value: any) { 100 | // @ts-ignore 101 | this.setState({ [type]: value, }); 102 | } 103 | 104 | _renderAppliedDirRow(dir: string) { 105 | // @ts-ignore 106 | const checked = this.state[`onSwiped${dir}Applied`]; 107 | // @ts-ignore 108 | const cssJs = {color: this.state[`onSwiped${dir}Applied`] ? '#000000' : '#cccccc'} 109 | return ( 110 | 111 | 112 | this.updateValue(`onSwiped${dir}Applied`, e.target.checked)} /> 114 | 115 | {dir} 116 | 117 | ) 118 | } 119 | 120 | render() { 121 | const { 122 | swiping, 123 | swiped, 124 | tapped, 125 | swipingDirection, 126 | swipedDirection, 127 | delta, 128 | showOnSwipeds, 129 | onSwipingApplied, 130 | onSwipedApplied, 131 | onTapApplied, 132 | preventScrollOnSwipe, 133 | trackTouch, 134 | trackMouse, 135 | rotationAngle, 136 | swipeDuration, 137 | stopScrollCss, 138 | } = this.state; 139 | 140 | const isSwipeDurationInfinity = swipeDuration === INFINITY; 141 | const swipeDurationTextValue = isSwipeDurationInfinity ? INFINITY : swipeDuration; 142 | const isSwipeDurationNumber = isSwipeDurationInfinity ? Infinity : !(isNaN(swipeDuration as any) || swipeDuration === ''); 143 | 144 | const isDeltaNumber = !(isNaN(delta as any) || delta === ''); 145 | const isRotationAngleNumber = !(isNaN(rotationAngle as any) || rotationAngle === ''); 146 | const deltaNum = isDeltaNumber ? +delta : 10; 147 | const rotationAngleNum = isRotationAngleNumber ? +rotationAngle : 0; 148 | 149 | const swipeableStyle = { 150 | fontSize: '0.75rem', 151 | touchAction: stopScrollCss ? 'none' : 'auto', 152 | }; 153 | 154 | const boundSwipes = getBoundSwipes(this); 155 | let swipeableDirProps: any = {}; 156 | if (onSwipingApplied) { 157 | // @ts-ignore 158 | swipeableDirProps.onSwiping = (...args: any)=>this.onSwiping(...args); 159 | } 160 | if (onSwipedApplied) { 161 | // @ts-ignore 162 | swipeableDirProps.onSwiped = (...args: any)=>this.onSwiped(...args); 163 | } 164 | if(onTapApplied) { 165 | // @ts-ignore 166 | swipeableDirProps.onTap = (...args: any) => this.onTap(...args); 167 | } 168 | 169 | return ( 170 |
171 |
172 |
💻 Test react-swipeable features.
173 | 184 |
this.resetState()} onMouseDown={()=>this.resetState()}> 185 |
Swipe inside here to test
186 |

See output below and check the console for 'onSwiping' and 'onSwiped' callback output(open dev tools)

187 | You can also 'toggle' the swiped props being applied to this container below. 188 |
189 |
190 | 191 | 192 | 193 | 194 | 195 | 196 | 200 | 201 | 202 | 203 | 207 | 208 | 209 | 210 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 226 | 227 | 228 | {showOnSwipeds && 229 | 240 | } 241 | 242 | 243 | 248 | 249 | 250 | 251 | 256 | 257 | 258 | 262 | 267 | 268 | 273 | 278 | 283 | 284 |
Applied?ActionOutput
197 | this.updateValue('onSwipingApplied', e.target.checked)} /> 199 | onSwiping{swiping ? 'True' : 'False'}
204 | this.updateValue('onSwipedApplied', e.target.checked)} /> 206 | onSwiped{swiped ? 'True' : 'False'}
211 | this.updateValue('onTapApplied', e.target.checked)} /> 213 | onTap{tapped ? 'True' : 'False'}
onSwiping Direction{swipingDirection}
222 | (e.preventDefault(),this.updateValue('showOnSwipeds', !showOnSwipeds))}> 223 | {showOnSwipeds ? '↑ Hide ↑' : '↓ Show ↓'} 224 | 225 | onSwiped Direction{swipedDirection}
230 | 231 | 232 | 233 | 234 | 235 | 236 | {DIRECTIONS.map(this._renderAppliedDirRow.bind(this))} 237 | 238 |
onSwiped
Applied?Direction
239 |
delta: 244 | this.updateValue('delta', getVal(e))} value={delta}/> 247 |
rotationAngle: 252 | this.updateValue('rotationAngle', getVal(e))} value={rotationAngle}/> 255 |
259 | swipeDuration: 260 |
(ms | Infinity)
261 |
263 | this.updateValue('swipeDuration', getVal(e))} value={swipeDurationTextValue}/> 266 |
285 | 286 | 287 | 293 | 294 |
295 | 296 |
297 |
298 | ) 299 | } 300 | } 301 | 302 | function getBoundSwipes(component: any) { 303 | let boundSwipes = {}; 304 | DIRECTIONS.forEach((dir)=>{ 305 | if (component.state[`onSwiped${dir}Applied`]) { 306 | // @ts-ignore 307 | boundSwipes[`onSwiped${dir}`] = component.onSwipedDirection.bind(component, dir); 308 | } 309 | }); 310 | return boundSwipes; 311 | } 312 | 313 | function getVal(e: any) { 314 | return e.target.value; 315 | } 316 | -------------------------------------------------------------------------------- /examples/app/SimpleCarousel/Carousel.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, ReactNode } from 'react'; 2 | import { useSwipeable } from 'react-swipeable'; 3 | import { 4 | Wrapper, 5 | CarouselContainer, 6 | CarouselSlot, 7 | SlideButtonContainer, 8 | SlideButton, 9 | PREV, 10 | NEXT 11 | } from '../components'; 12 | 13 | type Direction = typeof PREV | typeof NEXT; 14 | 15 | interface CarouselState { 16 | pos: number; 17 | sliding: boolean; 18 | dir: Direction; 19 | } 20 | 21 | type CarouselAction = 22 | | { type: Direction, numItems: number } 23 | | { type: 'stopSliding' }; 24 | 25 | const getOrder = (index: number, pos: number, numItems: number) => { 26 | return index - pos < 0 ? numItems - Math.abs(index - pos) : index - pos; 27 | }; 28 | 29 | const getInitialState = (numItems: number): CarouselState => ({ pos: numItems - 1, sliding: false, dir: NEXT }); 30 | 31 | const Carousel: FunctionComponent<{children: ReactNode}> = (props) => { 32 | const numItems = React.Children.count(props.children); 33 | const [state, dispatch] = React.useReducer(reducer, getInitialState(numItems)); 34 | 35 | const slide = (dir: Direction) => { 36 | dispatch({ type: dir, numItems }); 37 | setTimeout(() => { 38 | dispatch({ type: 'stopSliding' }); 39 | }, 50); 40 | }; 41 | 42 | const handlers = useSwipeable({ 43 | onSwipedLeft: () => slide(NEXT), 44 | onSwipedRight: () => slide(PREV), 45 | swipeDuration: 500, 46 | preventScrollOnSwipe: true, 47 | trackMouse: true 48 | }); 49 | 50 | return ( 51 |
52 | 53 | 54 | {React.Children.map(props.children, (child, index) => ( 55 | 58 | {child} 59 | 60 | ))} 61 | 62 | 63 | 64 | slide(PREV)} float="left"> 65 | Prev 66 | 67 | slide(NEXT)} float="right"> 68 | Next 69 | 70 | 71 |
72 | ); 73 | }; 74 | 75 | function reducer(state: CarouselState, action: CarouselAction): CarouselState { 76 | switch (action.type) { 77 | case PREV: 78 | return { 79 | ...state, 80 | dir: PREV, 81 | sliding: true, 82 | pos: state.pos === 0 ? action.numItems - 1 : state.pos - 1 83 | }; 84 | case NEXT: 85 | return { 86 | ...state, 87 | dir: NEXT, 88 | sliding: true, 89 | pos: state.pos === action.numItems - 1 ? 0 : state.pos + 1 90 | }; 91 | case 'stopSliding': 92 | return { ...state, sliding: false }; 93 | default: 94 | return state; 95 | } 96 | } 97 | 98 | export default Carousel; 99 | -------------------------------------------------------------------------------- /examples/app/SimpleCarousel/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Item } from '../components'; 3 | import Carousel from './Carousel'; 4 | // Carousel originally copied from: 5 | // https://medium.com/@incubation.ff/build-your-own-css-carousel-in-react-part-one-86f71f6670ca 6 | 7 | function SimpleCarousel() { 8 | return ( 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | ); 19 | } 20 | 21 | export default SimpleCarousel; 22 | -------------------------------------------------------------------------------- /examples/app/SimplePattern/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Item } from '../components'; 3 | import Carousel from './pattern'; 4 | // Carousel originally copied from: 5 | // https://medium.com/@incubation.ff/build-your-own-css-carousel-in-react-part-one-86f71f6670ca 6 | 7 | function SimplePattern() { 8 | return ( 9 |
10 |
11 | 👉 Swipe pattern 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | ); 22 | } 23 | 24 | export default SimplePattern; 25 | -------------------------------------------------------------------------------- /examples/app/SimplePattern/pattern.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, ReactNode } from 'react'; 2 | import { useSwipeable, UP, DOWN, SwipeEventData } from 'react-swipeable'; 3 | import { 4 | Wrapper, 5 | CarouselContainer, 6 | CarouselSlot, 7 | PatternBox, 8 | PREV, 9 | NEXT, 10 | D 11 | } from '../components'; 12 | 13 | const UpArrow = ({active}: {active: boolean}) => ( 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | 21 | const DownArrow = ({active}: {active: boolean}) => ( 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | 29 | 30 | type Direction = typeof PREV | typeof NEXT; 31 | 32 | interface CarouselState { 33 | pos: number; 34 | sliding: boolean; 35 | dir: Direction; 36 | } 37 | 38 | type CarouselAction = 39 | | { type: Direction, numItems: number } 40 | | { type: 'stopSliding' }; 41 | 42 | const getOrder = (index: number, pos: number, numItems: number) => { 43 | return index - pos < 0 ? numItems - Math.abs(index - pos) : index - pos; 44 | }; 45 | 46 | const pattern = [UP, DOWN, UP, DOWN]; 47 | 48 | const getInitialState = (numItems: number): CarouselState => ({ pos: numItems - 1, sliding: false, dir: NEXT }); 49 | 50 | const Carousel: FunctionComponent<{children: ReactNode}> = (props) => { 51 | const numItems = React.Children.count(props.children); 52 | const [state, dispatch] = React.useReducer(reducer, getInitialState(numItems)); 53 | 54 | const slide = (dir: Direction) => { 55 | dispatch({ type: dir, numItems }); 56 | setTimeout(() => { 57 | dispatch({ type: 'stopSliding' }); 58 | }, 50); 59 | }; 60 | 61 | const [pIdx, setPIdx] = React.useState(0); 62 | 63 | const handleSwiped = (eventData: SwipeEventData) => { 64 | if (eventData.dir === pattern[pIdx]) { 65 | // user successfully got to the end of the pattern! 66 | if (pIdx + 1 === pattern.length) { 67 | setPIdx(pattern.length); 68 | slide(NEXT); 69 | setTimeout(() => { 70 | setPIdx(0); 71 | }, 50); 72 | } else { 73 | // user is cont. with the pattern 74 | setPIdx((i) => (i += 1)); 75 | } 76 | } else { 77 | // user got the next pattern step wrong, reset pattern 78 | setPIdx(0); 79 | } 80 | }; 81 | 82 | const handlers = useSwipeable({ 83 | onSwiped: handleSwiped, 84 | onTouchStartOrOnMouseDown: (({ event }) => event.preventDefault()), 85 | touchEventOptions: { passive: false }, 86 | preventScrollOnSwipe: true, 87 | trackMouse: true 88 | }); 89 | 90 | return ( 91 | <> 92 | 93 | Within this text area container, swipe the pattern seen below to make 94 | the carousel navigate to the next slide. 95 | {`\n`} 96 |

97 | Swipe: 98 | 0} /> 99 | 1} /> 100 | 2} /> 101 | 3} /> 102 |

103 |
104 |
105 | 106 | 107 | {React.Children.map(props.children, (child, index) => ( 108 | 112 | {child} 113 | 114 | ))} 115 | 116 | 117 |
118 | 119 | ); 120 | }; 121 | 122 | function reducer(state: CarouselState, action: CarouselAction): CarouselState { 123 | switch (action.type) { 124 | case PREV: 125 | return { 126 | ...state, 127 | dir: PREV, 128 | sliding: true, 129 | pos: state.pos === 0 ? action.numItems - 1 : state.pos - 1 130 | }; 131 | case NEXT: 132 | return { 133 | ...state, 134 | dir: NEXT, 135 | sliding: true, 136 | pos: state.pos === action.numItems - 1 ? 0 : state.pos + 1 137 | }; 138 | case 'stopSliding': 139 | return { ...state, sliding: false }; 140 | default: 141 | return state; 142 | } 143 | } 144 | 145 | export default Carousel; 146 | -------------------------------------------------------------------------------- /examples/app/components.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Paper = styled.div` 4 | box-shadow: 0px 3px 3px -2px rgb(0 0 0 / 20%), 0px 3px 4px 0px rgb(0 0 0 / 14%), 0px 1px 8px 0px rgb(0 0 0 / 12%); 5 | border-radius: 4px; 6 | padding: 25px; 7 | `; 8 | 9 | export const NEXT = 'NEXT'; 10 | export const PREV = 'PREV'; 11 | 12 | export const Item = styled.div<{ img: string }>` 13 | text-align: center; 14 | padding: 100px; 15 | background-image: ${(props) => `url(${props.img})`}; 16 | background-size: cover; 17 | `; 18 | 19 | export const CarouselContainer = styled.div<{ sliding: boolean }>` 20 | display: flex; 21 | transition: ${(props) => (props.sliding ? "none" : "transform 1s ease")}; 22 | transform: ${(props) => { 23 | if (!props.sliding) return "translateX(calc(-80% - 20px))"; 24 | if (props.dir === PREV) return "translateX(calc(2 * (-80% - 20px)))"; 25 | return "translateX(0%)"; 26 | }}; 27 | `; 28 | 29 | export const Wrapper = styled.div` 30 | width: 100%; 31 | overflow: hidden; 32 | box-shadow: 5px 5px 20px 7px rgba(168, 168, 168, 1); 33 | `; 34 | 35 | export const CarouselSlot = styled.div<{ order: number }>` 36 | flex: 1 0 100%; 37 | flex-basis: 80%; 38 | margin-right: 20px; 39 | order: ${(props) => props.order}; 40 | `; 41 | 42 | export const SlideButtonContainer = styled.div` 43 | display: flex; 44 | justify-content: space-between; 45 | padding-bottom: 10px; 46 | `; 47 | 48 | export const SlideButton = styled.button<{ float: 'left' | 'right' }>` 49 | color: #ffffff; 50 | font-family: Open Sans; 51 | font-size: 16px; 52 | font-weight: 100; 53 | padding: 10px; 54 | background-color: #f66f3e; 55 | border: 1px solid white; 56 | text-decoration: none; 57 | display: inline-block; 58 | cursor: pointer; 59 | margin-top: 20px; 60 | text-decoration: none; 61 | 62 | &:active { 63 | position: relative; 64 | top: 1px; 65 | } 66 | &:focus { 67 | outline: 0; 68 | } 69 | `; 70 | 71 | export const PatternBox = styled.div` 72 | border: 2px solid black; 73 | width: 60%; 74 | margin: 20px auto; 75 | padding: 30px 20px; 76 | white-space: pre-line; 77 | `; 78 | 79 | export const D = styled.span` 80 | padding: 3px; 81 | `; 82 | -------------------------------------------------------------------------------- /examples/css/foundation.min.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8";/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */button,img,legend{border:0}body,button,legend{padding:0}.switch,[data-whatinput=mouse] .button,[data-whatinput=mouse] .close-button,[data-whatinput=mouse] .off-canvas,[data-whatinput=mouse] .reveal,[data-whatinput=mouse] .slider-handle,[data-whatinput=mouse] input:focus~.switch-paddle,a:active,a:hover{outline:0}.button-group::after,.clearfix::after,.off-canvas-wrapper-inner::after,.tabs::after,hr{clear:both}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}abbr[title]{border-bottom:1px dotted}b,optgroup,strong{font-weight:700}dfn{font-style:italic}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}.button,img{vertical-align:middle}sup{top:-.5em}sub{bottom:-.25em}img{max-width:100%;height:auto;-ms-interpolation-mode:bicubic;display:inline-block}svg:not(:root){overflow:hidden}figure{margin:1em 40px}pre,textarea{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}a,b,em,i,small,strong{line-height:inherit}dl,ol,p,ul{line-height:1.6}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}.foundation-mq{font-family:"small=0em&medium=40em&large=64em&xlarge=75em&xxlarge=90em"}body,h1,h2,h3,h4,h5,h6{font-family:"Helvetica Neue",Helvetica,Roboto,Arial,sans-serif;font-weight:400;color:#222}html{font-size:100%;box-sizing:border-box}*,:after,:before{box-sizing:inherit}body{margin:0;line-height:1.5;background:#fefefe;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}select{width:100%}#map_canvas embed,#map_canvas img,#map_canvas object,.map_canvas embed,.map_canvas img,.map_canvas object,.mqa-display embed,.mqa-display img,.mqa-display object{max-width:none!important}button{overflow:visible;-webkit-appearance:none;-moz-appearance:none;background:0 0;border-radius:3px;line-height:1}.row{max-width:62.5rem;margin-left:auto;margin-right:auto;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap}.column-row .row,.row .row{margin-left:-.625rem;margin-right:-.625rem}.row.expanded{max-width:none}.row.collapse>.column,.row.collapse>.columns{padding-left:0;padding-right:0}.column,.columns{padding-left:.625rem;padding-right:.625rem;-webkit-flex:1 1 0px;-ms-flex:1 1 0px;flex:1 1 0px}@media screen and (min-width:40em){.column-row .row,.row .row{margin-left:-.9375rem;margin-right:-.9375rem}.column,.columns{padding-left:.9375rem;padding-right:.9375rem}}.small-1{-webkit-flex:0 0 8.33333%;-ms-flex:0 0 8.33333%;flex:0 0 8.33333%;max-width:8.33333%}.small-offset-0{margin-left:0}.small-2{-webkit-flex:0 0 16.66667%;-ms-flex:0 0 16.66667%;flex:0 0 16.66667%;max-width:16.66667%}.small-offset-1{margin-left:8.33333%}.small-3{-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.small-offset-2{margin-left:16.66667%}.small-4{-webkit-flex:0 0 33.33333%;-ms-flex:0 0 33.33333%;flex:0 0 33.33333%;max-width:33.33333%}.small-offset-3{margin-left:25%}.small-5{-webkit-flex:0 0 41.66667%;-ms-flex:0 0 41.66667%;flex:0 0 41.66667%;max-width:41.66667%}.small-offset-4{margin-left:33.33333%}.small-6{-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.small-offset-5{margin-left:41.66667%}.small-7{-webkit-flex:0 0 58.33333%;-ms-flex:0 0 58.33333%;flex:0 0 58.33333%;max-width:58.33333%}.small-offset-6{margin-left:50%}.small-8{-webkit-flex:0 0 66.66667%;-ms-flex:0 0 66.66667%;flex:0 0 66.66667%;max-width:66.66667%}.small-offset-7{margin-left:58.33333%}.small-9{-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.small-offset-8{margin-left:66.66667%}.small-10{-webkit-flex:0 0 83.33333%;-ms-flex:0 0 83.33333%;flex:0 0 83.33333%;max-width:83.33333%}.small-offset-9{margin-left:75%}.small-11{-webkit-flex:0 0 91.66667%;-ms-flex:0 0 91.66667%;flex:0 0 91.66667%;max-width:91.66667%}.small-offset-10{margin-left:83.33333%}.small-12{-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.small-offset-11{margin-left:91.66667%}.small-order-1{-webkit-order:1;-ms-flex-order:1;order:1}.small-order-2{-webkit-order:2;-ms-flex-order:2;order:2}.small-order-3{-webkit-order:3;-ms-flex-order:3;order:3}.small-order-4{-webkit-order:4;-ms-flex-order:4;order:4}.small-order-5{-webkit-order:5;-ms-flex-order:5;order:5}.small-order-6{-webkit-order:6;-ms-flex-order:6;order:6}.small-collapse>.column,.small-collapse>.columns{padding-left:0;padding-right:0}.small-uncollapse>.column,.small-uncollapse>.columns{padding-left:.625rem;padding-right:.625rem}@media screen and (min-width:40em){.medium-1{-webkit-flex:0 0 8.33333%;-ms-flex:0 0 8.33333%;flex:0 0 8.33333%;max-width:8.33333%}.medium-offset-0{margin-left:0}.medium-2{-webkit-flex:0 0 16.66667%;-ms-flex:0 0 16.66667%;flex:0 0 16.66667%;max-width:16.66667%}.medium-offset-1{margin-left:8.33333%}.medium-3{-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.medium-offset-2{margin-left:16.66667%}.medium-4{-webkit-flex:0 0 33.33333%;-ms-flex:0 0 33.33333%;flex:0 0 33.33333%;max-width:33.33333%}.medium-offset-3{margin-left:25%}.medium-5{-webkit-flex:0 0 41.66667%;-ms-flex:0 0 41.66667%;flex:0 0 41.66667%;max-width:41.66667%}.medium-offset-4{margin-left:33.33333%}.medium-6{-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.medium-offset-5{margin-left:41.66667%}.medium-7{-webkit-flex:0 0 58.33333%;-ms-flex:0 0 58.33333%;flex:0 0 58.33333%;max-width:58.33333%}.medium-offset-6{margin-left:50%}.medium-8{-webkit-flex:0 0 66.66667%;-ms-flex:0 0 66.66667%;flex:0 0 66.66667%;max-width:66.66667%}.medium-offset-7{margin-left:58.33333%}.medium-9{-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.medium-offset-8{margin-left:66.66667%}.medium-10{-webkit-flex:0 0 83.33333%;-ms-flex:0 0 83.33333%;flex:0 0 83.33333%;max-width:83.33333%}.medium-offset-9{margin-left:75%}.medium-11{-webkit-flex:0 0 91.66667%;-ms-flex:0 0 91.66667%;flex:0 0 91.66667%;max-width:91.66667%}.medium-offset-10{margin-left:83.33333%}.medium-12{-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.medium-offset-11{margin-left:91.66667%}.medium-order-1{-webkit-order:1;-ms-flex-order:1;order:1}.medium-order-2{-webkit-order:2;-ms-flex-order:2;order:2}.medium-order-3{-webkit-order:3;-ms-flex-order:3;order:3}.medium-order-4{-webkit-order:4;-ms-flex-order:4;order:4}.medium-order-5{-webkit-order:5;-ms-flex-order:5;order:5}.medium-order-6{-webkit-order:6;-ms-flex-order:6;order:6}}@media screen and (min-width:40em) and (min-width:40em){.medium-expand{-webkit-flex:1 1 0px;-ms-flex:1 1 0px;flex:1 1 0px}}.row.medium-unstack .column,.row.medium-unstack .columns{-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%}@media screen and (min-width:40em){.row.medium-unstack .column,.row.medium-unstack .columns{-webkit-flex:1 1 0px;-ms-flex:1 1 0px;flex:1 1 0px}.medium-collapse>.column,.medium-collapse>.columns{padding-left:0;padding-right:0}.medium-uncollapse>.column,.medium-uncollapse>.columns{padding-left:.9375rem;padding-right:.9375rem}}@media screen and (min-width:64em){.large-1{-webkit-flex:0 0 8.33333%;-ms-flex:0 0 8.33333%;flex:0 0 8.33333%;max-width:8.33333%}.large-offset-0{margin-left:0}.large-2{-webkit-flex:0 0 16.66667%;-ms-flex:0 0 16.66667%;flex:0 0 16.66667%;max-width:16.66667%}.large-offset-1{margin-left:8.33333%}.large-3{-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.large-offset-2{margin-left:16.66667%}.large-4{-webkit-flex:0 0 33.33333%;-ms-flex:0 0 33.33333%;flex:0 0 33.33333%;max-width:33.33333%}.large-offset-3{margin-left:25%}.large-5{-webkit-flex:0 0 41.66667%;-ms-flex:0 0 41.66667%;flex:0 0 41.66667%;max-width:41.66667%}.large-offset-4{margin-left:33.33333%}.large-6{-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.large-offset-5{margin-left:41.66667%}.large-7{-webkit-flex:0 0 58.33333%;-ms-flex:0 0 58.33333%;flex:0 0 58.33333%;max-width:58.33333%}.large-offset-6{margin-left:50%}.large-8{-webkit-flex:0 0 66.66667%;-ms-flex:0 0 66.66667%;flex:0 0 66.66667%;max-width:66.66667%}.large-offset-7{margin-left:58.33333%}.large-9{-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.large-offset-8{margin-left:66.66667%}.large-10{-webkit-flex:0 0 83.33333%;-ms-flex:0 0 83.33333%;flex:0 0 83.33333%;max-width:83.33333%}.large-offset-9{margin-left:75%}.large-11{-webkit-flex:0 0 91.66667%;-ms-flex:0 0 91.66667%;flex:0 0 91.66667%;max-width:91.66667%}.large-offset-10{margin-left:83.33333%}.large-12{-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.large-offset-11{margin-left:91.66667%}.large-order-1{-webkit-order:1;-ms-flex-order:1;order:1}.large-order-2{-webkit-order:2;-ms-flex-order:2;order:2}.large-order-3{-webkit-order:3;-ms-flex-order:3;order:3}.large-order-4{-webkit-order:4;-ms-flex-order:4;order:4}.large-order-5{-webkit-order:5;-ms-flex-order:5;order:5}.large-order-6{-webkit-order:6;-ms-flex-order:6;order:6}}ol,ul{margin-left:1.25rem}@media screen and (min-width:64em) and (min-width:64em){.large-expand{-webkit-flex:1 1 0px;-ms-flex:1 1 0px;flex:1 1 0px}}.row.large-unstack .column,.row.large-unstack .columns{-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%}@media screen and (min-width:64em){.row.large-unstack .column,.row.large-unstack .columns{-webkit-flex:1 1 0px;-ms-flex:1 1 0px;flex:1 1 0px}.large-collapse>.column,.large-collapse>.columns{padding-left:0;padding-right:0}.large-uncollapse>.column,.large-uncollapse>.columns{padding-left:.9375rem;padding-right:.9375rem}}.shrink{-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto}.row.align-right{-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end}.row.align-center{-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}.row.align-justify{-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.row.align-spaced{-webkit-justify-content:space-around;-ms-flex-pack:distribute;justify-content:space-around}.row.align-top{-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start}.align-top.columns,.column.align-top{-webkit-align-self:flex-start;-ms-flex-item-align:start;align-self:flex-start}.row.align-bottom{-webkit-align-items:flex-end;-ms-flex-align:end;align-items:flex-end}.align-bottom.columns,.column.align-bottom{-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end}.row.align-middle{-webkit-align-items:center;-ms-flex-align:center;align-items:center}.align-middle.columns,.column.align-middle{-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}.row.align-stretch{-webkit-align-items:stretch;-ms-flex-align:stretch;align-items:stretch}.align-stretch.columns,.column.align-stretch{-webkit-align-self:stretch;-ms-flex-item-align:stretch;align-self:stretch}blockquote,dd,div,dl,dt,form,h1,h2,h3,h4,h5,h6,li,ol,p,pre,td,th,ul{margin:0;padding:0}dl,ol,p,ul{margin-bottom:1rem}p{font-size:inherit;text-rendering:optimizeLegibility}em,i{font-style:italic}h1,h2,h3,h4,h5,h6{font-style:normal;text-rendering:optimizeLegibility;margin-top:0;margin-bottom:.5rem;line-height:1.4}code,kbd{background-color:#e6e6e6;color:#0a0a0a;font-family:Consolas,"Liberation Mono",Courier,monospace}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{color:#cacaca;line-height:0}h1{font-size:1.5rem}h2{font-size:1.25rem}h3{font-size:1.1875rem}h4{font-size:1.125rem}h5{font-size:1.0625rem}h6{font-size:1rem}@media screen and (min-width:40em){h1{font-size:3rem}h2{font-size:2.5rem}h3{font-size:1.9375rem}h4{font-size:1.5625rem}h5{font-size:1.25rem}h6{font-size:1rem}}a{background-color:transparent;color:#2ba6cb;cursor:pointer}a:focus,a:hover{color:#258faf}a img{border:0}hr{box-sizing:content-box;max-width:62.5rem;height:0;border-right:0;border-top:0;border-bottom:1px solid #cacaca;border-left:0;margin:1.25rem auto}dl,ol,ul{list-style-position:outside}li{font-size:inherit}ul{list-style-type:disc}.accordion,.tabs{list-style-type:none}ol ol,ol ul,ul ol,ul ul{margin-left:1.25rem;margin-bottom:0}dl dt{margin-bottom:.3rem;font-weight:700}.subheader,code,label{font-weight:400}blockquote{margin:0 0 1rem;padding:.5625rem 1.25rem 0 1.1875rem;border-left:1px solid #cacaca}blockquote,blockquote p{line-height:1.6;color:#8a8a8a}cite{display:block;font-size:.8125rem;color:#8a8a8a}cite:before{content:'\2014 \0020'}abbr{color:#222;cursor:help;border-bottom:1px dotted #0a0a0a}code{border:1px solid #cacaca;padding:.125rem .3125rem .0625rem}kbd{padding:.125rem .25rem 0;margin:0}.subheader{margin-top:.2rem;margin-bottom:.5rem;line-height:1.4;color:#8a8a8a}.lead{font-size:125%;line-height:1.6}.button,.stat{line-height:1}.stat{font-size:2.5rem}p+.stat{margin-top:-1rem}.no-bullet{margin-left:0;list-style:none}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}@media screen and (min-width:40em){.medium-text-left{text-align:left}.medium-text-right{text-align:right}.medium-text-center{text-align:center}.medium-text-justify{text-align:justify}}@media screen and (min-width:64em){.large-text-left{text-align:left}.large-text-right{text-align:right}.large-text-center{text-align:center}.large-text-justify{text-align:justify}}.badge,.button,.input-group-label{text-align:center}.show-for-print{display:none!important}@media print{blockquote,img,pre,tr{page-break-inside:avoid}*{background:0 0!important;color:#000!important;box-shadow:none!important;text-shadow:none!important}.show-for-print{display:block!important}.hide-for-print{display:none!important}table.show-for-print{display:table!important}thead.show-for-print{display:table-header-group!important}tbody.show-for-print{display:table-row-group!important}tr.show-for-print{display:table-row!important}td.show-for-print,th.show-for-print{display:table-cell!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}.ir a:after,a[href^='javascript:']:after,a[href^='#']:after{content:''}abbr[title]:after{content:" (" attr(title) ")"}blockquote,pre{border:1px solid #999}thead{display:table-header-group}img{max-width:100%!important}@page{margin:.5cm}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}}.button{display:inline-block;cursor:pointer;-webkit-appearance:none;transition:background-color .25s ease-out,color .25s ease-out;border:1px solid transparent;border-radius:3px;padding:.85em 1em;margin:0 0 1rem;font-size:.9rem;background-color:#2ba6cb;color:#fff}.button:focus,.button:hover{background-color:#258dad;color:#fff}.button.tiny{font-size:.6rem}.button.small{font-size:.75rem}.button.large{font-size:1.25rem}.button.expanded{display:block;width:100%;margin-left:0;margin-right:0}.button.primary{background-color:#2ba6cb;color:#fff}.button.primary:focus,.button.primary:hover{background-color:#2285a2;color:#fff}.button.secondary{background-color:#e9e9e9;color:#000}.button.secondary:focus,.button.secondary:hover{background-color:#bababa;color:#000}.button.success{background-color:#5da423;color:#fff}.button.success:focus,.button.success:hover{background-color:#4a831c;color:#fff}.button.alert{background-color:#c60f13;color:#fff}.button.alert:focus,.button.alert:hover{background-color:#9e0c0f;color:#fff}.button.warning{background-color:#ffae00;color:#fff}.button.warning:focus,.button.warning:hover{background-color:#cc8b00;color:#fff}.button.hollow{border:1px solid #2ba6cb;color:#2ba6cb}.button.hollow,.button.hollow:focus,.button.hollow:hover{background-color:transparent}.button.hollow:focus,.button.hollow:hover{border-color:#165366;color:#165366}.button.hollow.primary{border:1px solid #2ba6cb;color:#2ba6cb}.button.hollow.primary:focus,.button.hollow.primary:hover{border-color:#165366;color:#165366}.button.hollow.secondary{border:1px solid #e9e9e9;color:#e9e9e9}.button.hollow.secondary:focus,.button.hollow.secondary:hover{border-color:#757575;color:#757575}.button.hollow.success{border:1px solid #5da423;color:#5da423}.button.hollow.success:focus,.button.hollow.success:hover{border-color:#2f5212;color:#2f5212}.button.hollow.alert{border:1px solid #c60f13;color:#c60f13}.button.hollow.alert:focus,.button.hollow.alert:hover{border-color:#63080a;color:#63080a}.button.hollow.warning{border:1px solid #ffae00;color:#ffae00}.button.hollow.warning:focus,.button.hollow.warning:hover{border-color:#805700;color:#805700}.button.disabled,.button[disabled]{opacity:.25;cursor:not-allowed;pointer-events:none}.button.dropdown::after{content:'';width:0;height:0;border:.4em inset;border-color:#fefefe transparent transparent;border-top-style:solid;position:relative;top:.4em;float:right;margin-left:1em;display:inline-block}.button.arrow-only::after{margin-left:0;float:none;top:.2em}[type=text],[type=password],[type=date],[type=datetime],[type=datetime-local],[type=month],[type=week],[type=email],[type=number],[type=search],[type=tel],[type=time],[type=url],[type=color],textarea{display:block;box-sizing:border-box;width:100%;height:2.4375rem;padding:.5rem;border:1px solid #cacaca;margin:0 0 1rem;font-family:inherit;font-size:1rem;color:#0a0a0a;background-color:#fefefe;box-shadow:inset 0 1px 2px rgba(10,10,10,.1);border-radius:3px;transition:box-shadow .5s,border-color .25s ease-in-out;-webkit-appearance:none;-moz-appearance:none}[type=text]:focus,[type=password]:focus,[type=date]:focus,[type=datetime]:focus,[type=datetime-local]:focus,[type=month]:focus,[type=week]:focus,[type=email]:focus,[type=number]:focus,[type=search]:focus,[type=tel]:focus,[type=time]:focus,[type=url]:focus,[type=color]:focus,textarea:focus{border:1px solid #8a8a8a;background-color:#fefefe;outline:0;box-shadow:0 0 5px #cacaca;transition:box-shadow .5s,border-color .25s ease-in-out}textarea{min-height:50px;max-width:100%}textarea[rows]{height:auto}input:disabled,input[readonly],textarea:disabled,textarea[readonly]{background-color:#e6e6e6;cursor:default}[type=submit],[type=button]{border-radius:3px;-webkit-appearance:none;-moz-appearance:none}input[type=search]{box-sizing:border-box}[type=file],[type=checkbox],[type=radio]{margin:0 0 1rem}[type=checkbox]+label,[type=radio]+label{display:inline-block;margin-left:.5rem;margin-right:1rem;margin-bottom:0;vertical-align:baseline}label>[type=checkbox],label>[type=label]{margin-right:.5rem}[type=file]{width:100%}label{display:block;margin:0;font-size:.875rem;line-height:1.8;color:#0a0a0a}label.middle{margin:0 0 1rem;padding:.5625rem 0}.help-text{margin-top:-.5rem;font-size:.8125rem;font-style:italic;color:#333}.input-group{display:table;width:100%;margin-bottom:1rem}.input-group-button a,.input-group-button button,.input-group-button input,fieldset{margin:0}.input-group>:first-child{border-radius:3px 0 0 3px}.input-group>:last-child>*{border-radius:0 3px 3px 0}.input-group-button,.input-group-field,.input-group-label{display:table-cell;margin:0;vertical-align:middle}.input-group-label{width:1%;height:100%;padding:0 1rem;background:#e6e6e6;color:#0a0a0a;border:1px solid #cacaca}.input-group-label:first-child{border-right:0}.input-group-label:last-child{border-left:0}.input-group-field{border-radius:0;height:2.5rem}.fieldset,select{border:1px solid #cacaca}.input-group-button{height:100%;padding-top:0;padding-bottom:0;text-align:center;width:1%}fieldset{border:0;padding:0}legend{margin-bottom:.5rem}.fieldset{padding:1.25rem;margin:1.125rem 0}.fieldset legend{background:#fefefe;padding:0 .1875rem;margin:0 0 0 -.1875rem}select{height:2.4375rem;padding:.5rem;margin:0 0 1rem;font-size:1rem;font-family:inherit;line-height:normal;color:#0a0a0a;background-color:#fefefe;border-radius:3px;-webkit-appearance:none;-moz-appearance:none;background-image:url('data:image/svg+xml;utf8,');background-size:9px 6px;background-position:right .5rem center;background-repeat:no-repeat}.form-error,.is-invalid-label{color:#c60f13}@media screen and (min-width:0\0){select{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAYCAYAAACbU/80AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAIpJREFUeNrEkckNgDAMBBfRkEt0ObRBBdsGXUDgmQfK4XhH2m8czQAAy27R3tsw4Qfe2x8uOO6oYLb6GlOor3GF+swURAOmUJ+RwtEJs9WvTGEYxBXqI1MQAZhCfUQKRzDMVj+TwrAIV6jvSUEkYAr1LSkcyTBb/V+KYfX7xAeusq3sLDtGH3kEGACPWIflNZfhRQAAAABJRU5ErkJggg==)}}select:disabled{background-color:#e6e6e6;cursor:default}select::-ms-expand{display:none}select[multiple]{height:auto}.is-invalid-input:not(:focus){background-color:rgba(198,15,19,.1);border-color:#c60f13}.form-error{display:none;margin-top:-.5rem;margin-bottom:1rem;font-size:.75rem;font-weight:700}.form-error.is-visible{display:block}.hide{display:none!important}.invisible{visibility:hidden}@media screen and (min-width:0em) and (max-width:39.9375em){.hide-for-small-only{display:none!important}}@media screen and (max-width:0em),screen and (min-width:40em){.show-for-small-only{display:none!important}}@media screen and (min-width:40em){.hide-for-medium{display:none!important}}@media screen and (max-width:39.9375em){.show-for-medium{display:none!important}}@media screen and (min-width:40em) and (max-width:63.9375em){.hide-for-medium-only{display:none!important}}@media screen and (max-width:39.9375em),screen and (min-width:64em){.show-for-medium-only{display:none!important}}@media screen and (min-width:64em){.hide-for-large{display:none!important}}@media screen and (max-width:63.9375em){.show-for-large{display:none!important}}@media screen and (min-width:64em) and (max-width:74.9375em){.hide-for-large-only{display:none!important}}@media screen and (max-width:63.9375em),screen and (min-width:75em){.show-for-large-only{display:none!important}}.show-for-sr,.show-on-focus{position:absolute!important;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}.show-on-focus:active,.show-on-focus:focus{position:static!important;height:auto;width:auto;overflow:visible;clip:auto}.hide-for-portrait,.show-for-landscape{display:block!important}@media screen and (orientation:landscape){.hide-for-portrait,.show-for-landscape{display:block!important}.hide-for-landscape,.show-for-portrait{display:none!important}}.hide-for-landscape,.show-for-portrait{display:none!important}@media screen and (orientation:portrait){.hide-for-portrait,.show-for-landscape{display:none!important}.hide-for-landscape,.show-for-portrait{display:block!important}}.float-left{float:left!important}.float-right{float:right!important}.float-center{display:block;margin-left:auto;margin-right:auto}.clearfix::after,.clearfix::before{content:' ';display:table}.accordion{background:#fefefe;border:1px solid #e6e6e6;border-radius:3px;margin-left:0}.accordion-title{display:block;padding:1.25rem 1rem;line-height:1;font-size:.75rem;color:#2ba6cb;position:relative;border-bottom:1px solid #e6e6e6}.accordion-title:focus,.accordion-title:hover{background-color:#e6e6e6}:last-child>.accordion-title{border-bottom-width:0}.accordion-title::before{content:'+';position:absolute;right:1rem;top:50%;margin-top:-.5rem}.is-active>.accordion-title::before{content:'–'}.accordion-content{padding:1rem;display:none;border-bottom:1px solid #e6e6e6;background-color:#fefefe}.badge{display:inline-block;padding:.3em;min-width:2.1em;font-size:.6rem;border-radius:50%;background:#2ba6cb;color:#fefefe}.badge.secondary{background:#e9e9e9;color:#0a0a0a}.badge.success{background:#5da423;color:#fefefe}.badge.alert{background:#c60f13;color:#fefefe}.badge.warning{background:#ffae00;color:#fefefe}.button-group{margin-bottom:1rem;font-size:.9rem}.button-group::after,.button-group::before{content:' ';display:table}.button-group .button{float:left;margin:0;font-size:inherit}.button-group .button:not(:last-child){border-right:1px solid #fefefe}.button-group.tiny{font-size:.6rem}.button-group.small{font-size:.75rem}.button-group.large{font-size:1.25rem}.button-group.expanded{display:table;table-layout:fixed;width:100%}.button-group.expanded::after,.button-group.expanded::before{display:none}.button-group.expanded .button{display:table-cell;float:none}.button-group.primary .button{background-color:#2ba6cb;color:#fff}.button-group.primary .button:focus,.button-group.primary .button:hover{background-color:#2285a2;color:#fff}.button-group.secondary .button{background-color:#e9e9e9;color:#000}.button-group.secondary .button:focus,.button-group.secondary .button:hover{background-color:#bababa;color:#000}.button-group.success .button{background-color:#5da423;color:#fff}.button-group.success .button:focus,.button-group.success .button:hover{background-color:#4a831c;color:#fff}.button-group.alert .button{background-color:#c60f13;color:#fff}.button-group.alert .button:focus,.button-group.alert .button:hover{background-color:#9e0c0f;color:#fff}.button-group.warning .button{background-color:#ffae00;color:#fff}.button-group.warning .button:focus,.button-group.warning .button:hover{background-color:#cc8b00;color:#fff}.button-group.stacked .button,.button-group.stacked-for-small .button{width:100%}.button-group.stacked .button:not(:last-child),.button-group.stacked-for-small .button:not(:last-child){border-right:1px solid}@media screen and (min-width:40em){.button-group.stacked-for-small .button{width:auto}.button-group.stacked-for-small .button:not(:last-child){border-right:1px solid #fefefe}}.callout{margin:0 0 1rem;padding:1rem;border:1px solid rgba(10,10,10,.25);border-radius:3px;position:relative;color:#222;background-color:#fff}.callout>:first-child{margin-top:0}.callout>:last-child{margin-bottom:0}.callout.primary{background-color:#def2f8}.callout.secondary{background-color:#fcfcfc}.callout.success{background-color:#e6f7d9}.callout.alert{background-color:#fcd6d6}.callout.warning{background-color:#fff3d9}.callout.small{padding:.5rem}.callout.large{padding:3rem}.close-button{position:absolute;color:#8a8a8a;right:1rem;top:.5rem;font-size:2em;line-height:1;cursor:pointer}.close-button:focus,.close-button:hover{color:#0a0a0a}.dropdown-pane{background-color:#fefefe;border:1px solid #cacaca;display:block;padding:1rem;position:absolute;visibility:hidden;width:300px;z-index:10;border-radius:3px}.dropdown-pane.is-open{visibility:visible}.dropdown-pane.tiny{width:100px}.dropdown-pane.small{width:200px}.dropdown-pane.large{width:400px}.label{display:inline-block;padding:.33333rem .5rem;font-size:.8rem;line-height:1;white-space:nowrap;cursor:default;border-radius:3px;background:#2ba6cb;color:#fefefe}.label.secondary{background:#e9e9e9;color:#0a0a0a}.label.success{background:#5da423;color:#fefefe}.label.alert{background:#c60f13;color:#fefefe}.label.warning{background:#ffae00;color:#fefefe}.media-object{margin-bottom:1rem;display:block}.media-object img{max-width:none}@media screen and (min-width:0em) and (max-width:39.9375em){.media-object.stack-for-small .media-object-section{display:block;padding:0 0 1rem}.media-object.stack-for-small .media-object-section img{width:100%}}.media-object-section{display:table-cell;vertical-align:top}.media-object-section:first-child{padding-right:1rem}.media-object-section:last-child:not(+.media-object-section:first-child){padding-left:1rem}.media-object-section.middle{vertical-align:middle}.media-object-section.bottom{vertical-align:bottom}body,html{height:100%}.off-canvas-wrapper{width:100%;overflow-x:hidden;position:relative;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-overflow-scrolling:auto}.off-canvas-wrapper-inner{position:relative;width:100%;transition:-webkit-transform .5s ease;transition:transform .5s ease}.off-canvas-wrapper-inner::after,.off-canvas-wrapper-inner::before{content:' ';display:table}.off-canvas-content{min-height:100%;background:#fefefe;transition:-webkit-transform .5s ease;transition:transform .5s ease;-webkit-backface-visibility:hidden;backface-visibility:hidden;z-index:1;box-shadow:0 0 10px rgba(10,10,10,.5)}.js-off-canvas-exit{display:none;position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(254,254,254,.25);cursor:pointer;transition:background .5s ease}.is-off-canvas-open .js-off-canvas-exit{display:block}.off-canvas{position:absolute;background:#e6e6e6;z-index:-1;max-height:100%;overflow-y:auto;-webkit-transform:translateX(0);-ms-transform:translateX(0);transform:translateX(0)}.off-canvas.position-left{left:-250px;top:0;width:250px}.slider-fill,.slider-handle{left:0;display:inline-block}.is-open-left{-webkit-transform:translateX(250px);-ms-transform:translateX(250px);transform:translateX(250px)}.off-canvas.position-right{right:-250px;top:0;width:250px}.is-open-right{-webkit-transform:translateX(-250px);-ms-transform:translateX(-250px);transform:translateX(-250px)}@media screen and (min-width:40em){.position-left.reveal-for-medium{left:0;z-index:auto;position:fixed}.position-left.reveal-for-medium~.off-canvas-content{margin-left:250px}.position-right.reveal-for-medium{right:0;z-index:auto;position:fixed}.position-right.reveal-for-medium~.off-canvas-content{margin-right:250px}.reveal{min-height:0}}@media screen and (min-width:64em){.position-left.reveal-for-large{left:0;z-index:auto;position:fixed}.position-left.reveal-for-large~.off-canvas-content{margin-left:250px}.position-right.reveal-for-large{right:0;z-index:auto;position:fixed}.position-right.reveal-for-large~.off-canvas-content{margin-right:250px}}.slider{position:relative;height:.5rem;margin-top:1.25rem;margin-bottom:2.25rem;background-color:#e6e6e6;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-ms-touch-action:none;touch-action:none}.slider-fill{position:absolute;top:0;max-width:100%;height:.5rem;background-color:#cacaca;transition:all .2s ease-in-out}.slider-fill.is-dragging{transition:all 0s linear}.slider-handle{top:50%;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%);position:absolute;z-index:1;width:1.4rem;height:1.4rem;background-color:#2ba6cb;transition:all .2s ease-in-out;-ms-touch-action:manipulation;touch-action:manipulation;border-radius:3px}.slider-handle:hover{background-color:#258dad}.slider-handle.is-dragging{transition:all 0s linear}.slider.disabled,.slider[disabled]{opacity:.25;cursor:not-allowed}.slider.vertical{display:inline-block;width:.5rem;height:12.5rem;margin:0 1.25rem;-webkit-transform:scale(1,-1);-ms-transform:scale(1,-1);transform:scale(1,-1)}.slider.vertical .slider-fill{top:0;width:.5rem;max-height:100%}.slider.vertical .slider-handle{position:absolute;top:0;left:50%;width:1.4rem;height:1.4rem;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%)}body.is-reveal-open{overflow:hidden}.reveal-overlay{display:none;position:fixed;top:0;bottom:0;left:0;right:0;z-index:1005;background-color:rgba(10,10,10,.45);overflow-y:scroll}.reveal{display:none;z-index:1006;padding:1rem;border:1px solid #cacaca;margin:100px auto 0;background-color:#fefefe;border-radius:3px;position:absolute;overflow-y:auto}.switch-paddle,.switch-paddle::after{display:block;transition:all .25s ease-out}.reveal .column,.reveal .columns{min-width:0}.reveal>:last-child{margin-bottom:0}.reveal.collapse{padding:0}@media screen and (min-width:40em){.reveal{width:600px;max-width:62.5rem}.reveal .reveal{left:auto;right:auto;margin:0 auto}.reveal.tiny{width:30%;max-width:62.5rem}.reveal.small{width:50%;max-width:62.5rem}.reveal.large{width:90%;max-width:62.5rem}}.reveal.full{top:0;left:0;width:100%;height:100%;height:100vh;min-height:100vh;max-width:none;margin-left:0;border:0}.switch{margin-bottom:1rem;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;color:#fefefe;font-weight:700;font-size:.875rem}.switch-input{opacity:0;position:absolute}.switch-paddle{background:#cacaca;cursor:pointer;position:relative;width:4rem;height:2rem;border-radius:3px;color:inherit;font-weight:inherit}caption,tfoot td,tfoot th,thead td,thead th{font-weight:700;padding:.5rem .625rem .625rem}input+.switch-paddle{margin:0}.switch-paddle::after{background:#fefefe;content:'';position:absolute;height:1.5rem;left:.25rem;top:.25rem;width:1.5rem;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);border-radius:3px}input:checked~.switch-paddle{background:#2ba6cb}input:checked~.switch-paddle::after{left:2.25rem}.switch-active,.switch-inactive{position:absolute;top:50%;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%)}.switch-active{left:8%;display:none}input:checked+label>.switch-active{display:block}.switch-inactive{right:15%}input:checked+label>.switch-inactive{display:none}.switch.tiny .switch-paddle{width:3rem;height:1.5rem;font-size:.625rem}.switch.tiny .switch-paddle::after{width:1rem;height:1rem}.switch.tiny input:checked~.switch-paddle:after{left:1.75rem}.switch.small .switch-paddle{width:3.5rem;height:1.75rem;font-size:.75rem}.switch.small .switch-paddle::after{width:1.25rem;height:1.25rem}.switch.small input:checked~.switch-paddle:after{left:2rem}.switch.large .switch-paddle{width:5rem;height:2.5rem;font-size:1rem}.switch.large .switch-paddle::after{width:2rem;height:2rem}.switch.large input:checked~.switch-paddle:after{left:2.75rem}table{border-collapse:collapse;border-spacing:0;margin-bottom:1rem;border-radius:3px}tbody,tfoot,thead{border:1px solid #f1f1f1;background-color:#fefefe}tfoot,thead{background:#f8f8f8;color:#222}tfoot tr,thead tr{background:0 0}tfoot td,tfoot th,thead td,thead th{text-align:left}tbody tr:nth-child(even){background-color:#f1f1f1}tbody td,tbody th{padding:.5rem .625rem .625rem}@media screen and (max-width:63.9375em){table.stack tfoot,table.stack thead{display:none}table.stack td,table.stack th,table.stack tr{display:block}table.stack td{border-top:0}}.tabs,.tabs-content{border:1px solid #e6e6e6}table.scroll{display:block;width:100%;overflow-x:auto}table.hover tr:hover{background-color:#f9f9f9}table.hover tr:nth-of-type(even):hover{background-color:#ececec}.tabs{margin:0;background:#fefefe}.tabs::after,.tabs::before{content:' ';display:table}.tabs.vertical>li{width:auto;float:none;display:block}.tabs.simple>li>a{padding:0}.tabs.simple>li>a:hover{background:0 0}.tabs.primary{background:#2ba6cb}.tabs.primary>li>a{color:#fefefe}.tabs.primary>li>a:focus,.tabs.primary>li>a:hover{background:#299ec1}.tabs-title{float:left}.tabs-title>a{display:block;padding:1.25rem 1.5rem;line-height:1;font-size:12px;color:#2ba6cb}.tabs-title>a:hover{background:#fefefe}.tabs-title>a:focus,.tabs-title>a[aria-selected=true]{background:#e6e6e6}.tabs-content{background:#fefefe;transition:all .5s ease;border-top:0}.tabs-content.vertical{border:1px solid #e6e6e6;border-left:0}.tabs-panel{display:none;padding:1rem}.tabs-panel.is-active{display:block} 2 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | react swipeable examples 6 | 7 | 8 | 13 | 14 | 15 |
16 | 17 | 18 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /examples/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOMClient from 'react-dom/client'; 3 | import App from './app/App'; 4 | 5 | const container = document.getElementById('app'); 6 | 7 | // Create a root. 8 | // @ts-ignore 9 | const root = ReactDOMClient.createRoot(container); 10 | 11 | // Initial render: Render an element to the root. 12 | // @ts-ignore 13 | root.render(); 14 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "start": "webpack-dev-server", 5 | "start:dev:local": "DEV_LOCAL=true webpack-dev-server", 6 | "build": "webpack -p --config ./webpack.config.min.js", 7 | "build:local": "DEV_LOCAL=true webpack -p --config ./webpack.config.js" 8 | }, 9 | "dependencies": { 10 | "react": "^18.1.0", 11 | "react-dom": "^18.1.0", 12 | "react-swipeable": "^7.0.0", 13 | "styled-components": "^5.3.5" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "^18.0.8", 17 | "@types/react-dom": "^18.0.0", 18 | "@types/styled-components": "^5.1.24", 19 | "ts-loader": "^8.0.1", 20 | "typescript": "^4.1.3", 21 | "webpack": "^4.29.0", 22 | "webpack-cli": "^3.2.1", 23 | "webpack-dev-server": "^3.1.14" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var WebpackDevServer = require('webpack-dev-server'); 3 | var config = require('./webpack.config'); 4 | 5 | new WebpackDevServer(webpack(config), { 6 | publicPath: config.output.publicPath, 7 | hot: true, 8 | historyApiFallback: true, 9 | stats: { 10 | colors: true 11 | } 12 | }).listen(3000, 'localhost', function (err) { 13 | if (err) { 14 | console.log(err); 15 | } 16 | 17 | console.log('Listening at localhost:3000'); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "baseUrl": ".", 5 | "esModuleInterop": true, 6 | "jsx": "react", 7 | "lib": ["ESNext", "dom"], 8 | "module": "ESNext", 9 | "moduleResolution": "node", 10 | "noImplicitAny": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "outDir": "lib/", 14 | "pretty": true, 15 | "skipLibCheck": true, 16 | "sourceMap": true, 17 | "strict": true, 18 | "target": "ESNext" 19 | }, 20 | "exclude": ["node_modules"], 21 | "include": ["./**/*"] 22 | } 23 | -------------------------------------------------------------------------------- /examples/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | 4 | const useLocalSwipeable = process.env.DEV_LOCAL === 'true'; 5 | 6 | if (useLocalSwipeable) { 7 | console.info('*****') 8 | console.info('NOTE: Using local copy of react-swipeable: src/index.ts') 9 | console.info('*****') 10 | } 11 | 12 | module.exports = { 13 | devtool: 'cheap-module-eval-source-map', 14 | entry: ['./index'], 15 | resolve: { 16 | extensions: [ '.tsx', '.ts', '.js' ], 17 | ...(useLocalSwipeable && { 18 | alias: { 19 | 'react-swipeable': path.resolve(__dirname, '../src/index.ts'), 20 | // Have to alias react to avoid two versions 21 | 'react': path.resolve(__dirname, '../examples/node_modules/react/index.js') 22 | } 23 | }) 24 | }, 25 | output: { 26 | path: path.join(__dirname, 'static'), 27 | publicPath: '/static/', 28 | filename: 'bundle.js' 29 | }, 30 | plugins: [ 31 | new webpack.DefinePlugin({ 32 | // retrieve react-swipeable version to display on demo page 33 | SWIPEABLE_VERSION: useLocalSwipeable ? JSON.stringify('DEV') : JSON.stringify( 34 | require('./node_modules/react-swipeable/package.json').version 35 | ) 36 | }) 37 | ], 38 | module: { 39 | rules: [ 40 | { 41 | test: /\.tsx?$/, 42 | use: 'ts-loader', 43 | exclude: /node_modules/, 44 | }, 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/webpack.config.min.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | 4 | module.exports = { 5 | devtool: 'eval', 6 | mode: 'production', 7 | entry: ['./index'], 8 | resolve: { 9 | extensions: [ '.tsx', '.ts', '.js' ], 10 | }, 11 | output: { 12 | path: path.join(__dirname, 'static'), 13 | publicPath: '/static/', 14 | filename: 'bundle.js' 15 | }, 16 | plugins: [ 17 | new webpack.DefinePlugin({ 18 | 'process.env.NODE_ENV': JSON.stringify('production'), 19 | // retrieve react-swipeable version to display on demo page 20 | SWIPEABLE_VERSION: JSON.stringify( 21 | require('./node_modules/react-swipeable/package.json').version 22 | ) 23 | }) 24 | ], 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.tsx?$/, 29 | use: 'ts-loader', 30 | exclude: /node_modules/, 31 | }, 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-swipeable", 3 | "version": "7.0.2", 4 | "description": "React Swipe event handler hook", 5 | "source": "./src/index.ts", 6 | "main": "./lib/index.js", 7 | "module": "es/index.js", 8 | "types": "./es/index.d.ts", 9 | "unpkg": "dist/react-swipeable.js", 10 | "sideEffects": false, 11 | "scripts": { 12 | "build": "yarn run build:types && yarn run build:outputs", 13 | "build:outputs": "rollup -c", 14 | "build:types": "tsc --project tsconfig.types.json", 15 | "check:ci": "yarn run prettier && yarn run lint && yarn run test:unit", 16 | "clean": "rimraf lib dist es", 17 | "examples:build": "cd ./examples && yarn && yarn build", 18 | "examples:build:publish": "yarn run examples:build && rimraf examples/node_modules && gh-pages -d examples", 19 | "prettier": "prettier --check src __tests__", 20 | "format": "prettier --write src __tests__", 21 | "lint": "eslint . --ext .ts,.tsx", 22 | "prebuild": "yarn run clean", 23 | "prepare": "yarn run build", 24 | "pretest": "yarn run prettier && yarn run lint", 25 | "preversion": "yarn test", 26 | "size": "size-limit", 27 | "start:examples": "cd ./examples && yarn && yarn start", 28 | "start:examples:local": "cd ./examples && yarn && yarn start:dev:local", 29 | "test": "yarn run test:unit && yarn run build && yarn run size", 30 | "test:unit": "jest", 31 | "test:unit:watch": "jest --watch", 32 | "test:cover": "jest --coverage" 33 | }, 34 | "size-limit": [ 35 | { 36 | "limit": "1.7 KB", 37 | "path": "lib/index.js" 38 | }, 39 | { 40 | "limit": "1.8 KB", 41 | "path": "dist/react-swipeable.js" 42 | }, 43 | { 44 | "limit": "1.8 KB", 45 | "path": "es/index.js" 46 | } 47 | ], 48 | "jest": { 49 | "preset": "ts-jest", 50 | "testEnvironment": "jsdom", 51 | "testMatch": [ 52 | "/__tests__/**/*.(test|spec).ts?(x)" 53 | ] 54 | }, 55 | "keywords": [ 56 | "react swipe", 57 | "react touch", 58 | "react hook", 59 | "touch", 60 | "swipe", 61 | "swipeable", 62 | "react", 63 | "hook" 64 | ], 65 | "authors": [ 66 | "Josh Perez (https://github.com/goatslacker)", 67 | "Brian Emil Hartz (https://github.com/hartzis)" 68 | ], 69 | "repository": { 70 | "type": "git", 71 | "url": "https://github.com/FormidableLabs/react-swipeable.git" 72 | }, 73 | "bugs": { 74 | "url": "https://github.com/FormidableLabs/react-swipeable/issues" 75 | }, 76 | "homepage": "https://github.com/FormidableLabs/react-swipeable", 77 | "files": [ 78 | "dist", 79 | "es", 80 | "lib", 81 | "src" 82 | ], 83 | "license": "MIT", 84 | "devDependencies": { 85 | "@changesets/cli": "^2.26.1", 86 | "@rollup/plugin-typescript": "^8.3.0", 87 | "@size-limit/preset-small-lib": "^7.0.8", 88 | "@svitejs/changesets-changelog-github-compact": "^0.1.1", 89 | "@testing-library/react": "^13.0.1", 90 | "@types/jest": "^27.4.1", 91 | "@types/react": "^18.0.8", 92 | "@types/react-dom": "^18.0.0", 93 | "@typescript-eslint/eslint-plugin": "^5.20.0", 94 | "@typescript-eslint/parser": "^5.20.0", 95 | "coveralls": "^3.0.3", 96 | "eslint": "^7.18.0", 97 | "eslint-config-prettier": "^6.11.0", 98 | "eslint-plugin-react": "^7.29.4", 99 | "eslint-plugin-react-hooks": "^4.4.0", 100 | "gh-pages": "^3.0.0", 101 | "jest": "^27.5.1", 102 | "prettier": "^2.0.5", 103 | "react": "^18.1.0", 104 | "react-dom": "^18.1.0", 105 | "rimraf": "^3.0.2", 106 | "rollup": "^2.60.0", 107 | "size-limit": "^7.0.8", 108 | "ts-jest": "^27.1.4", 109 | "typescript": "^4.6.3" 110 | }, 111 | "peerDependencies": { 112 | "react": "^16.8.3 || ^17 || ^18 || ^19.0.0 || ^19.0.0-rc" 113 | }, 114 | "publishConfig": { 115 | "provenance": true 116 | }, 117 | "engines": { 118 | "node": ">=18.0.0" 119 | } 120 | } -------------------------------------------------------------------------------- /react-swipeable-Hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/react-swipeable/d12491005fc1f68788c9e3b4ed2297d5ac2c5ae9/react-swipeable-Hero.png -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "@rollup/plugin-typescript"; 2 | import pkg from "./package.json"; 3 | 4 | export default [ 5 | // - CommonJS (for Node) 6 | // - ES module (for bundlers) 7 | // - UMD (for browser) 8 | { 9 | input: "src/index.ts", 10 | external: ["react"], 11 | output: [ 12 | // NOTE: interop: false is deprecated. 13 | // When we upgrade need to identify best path forward to avoid bloat. 14 | { file: pkg.main, format: "cjs", sourcemap: true, interop: false }, 15 | { file: pkg.module, format: "es", sourcemap: true }, 16 | { 17 | name: "swipeable", 18 | file: pkg.unpkg, 19 | format: "umd", 20 | sourcemap: true, 21 | globals: { 22 | react: 'React' 23 | }, 24 | interop: false, 25 | } 26 | ], 27 | // ts definition outputs specified in config 28 | plugins: [typescript({ tsconfig: './tsconfig.json' })], 29 | }, 30 | ]; 31 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* global document */ 2 | import * as React from "react"; 3 | import { 4 | AttachTouch, 5 | SwipeDirections, 6 | DOWN, 7 | SwipeEventData, 8 | HandledEvents, 9 | LEFT, 10 | RIGHT, 11 | Setter, 12 | ConfigurationOptions, 13 | SwipeableDirectionCallbacks, 14 | SwipeableHandlers, 15 | SwipeableProps, 16 | SwipeablePropsWithDefaultOptions, 17 | SwipeableState, 18 | SwipeCallback, 19 | TapCallback, 20 | UP, 21 | Vector2, 22 | } from "./types"; 23 | 24 | export { 25 | LEFT, 26 | RIGHT, 27 | UP, 28 | DOWN, 29 | SwipeDirections, 30 | SwipeEventData, 31 | SwipeableDirectionCallbacks, 32 | SwipeCallback, 33 | TapCallback, 34 | SwipeableHandlers, 35 | SwipeableProps, 36 | Vector2, 37 | }; 38 | 39 | const defaultProps: ConfigurationOptions = { 40 | delta: 10, 41 | preventScrollOnSwipe: false, 42 | rotationAngle: 0, 43 | trackMouse: false, 44 | trackTouch: true, 45 | swipeDuration: Infinity, 46 | touchEventOptions: { passive: true }, 47 | }; 48 | const initialState: SwipeableState = { 49 | first: true, 50 | initial: [0, 0], 51 | start: 0, 52 | swiping: false, 53 | xy: [0, 0], 54 | }; 55 | const mouseMove = "mousemove"; 56 | const mouseUp = "mouseup"; 57 | const touchEnd = "touchend"; 58 | const touchMove = "touchmove"; 59 | const touchStart = "touchstart"; 60 | 61 | function getDirection( 62 | absX: number, 63 | absY: number, 64 | deltaX: number, 65 | deltaY: number 66 | ): SwipeDirections { 67 | if (absX > absY) { 68 | if (deltaX > 0) { 69 | return RIGHT; 70 | } 71 | return LEFT; 72 | } else if (deltaY > 0) { 73 | return DOWN; 74 | } 75 | return UP; 76 | } 77 | 78 | function rotateXYByAngle(pos: Vector2, angle: number): Vector2 { 79 | if (angle === 0) return pos; 80 | const angleInRadians = (Math.PI / 180) * angle; 81 | const x = 82 | pos[0] * Math.cos(angleInRadians) + pos[1] * Math.sin(angleInRadians); 83 | const y = 84 | pos[1] * Math.cos(angleInRadians) - pos[0] * Math.sin(angleInRadians); 85 | return [x, y]; 86 | } 87 | 88 | function getHandlers( 89 | set: Setter, 90 | handlerProps: { trackMouse: boolean | undefined } 91 | ): [ 92 | { 93 | ref: (element: HTMLElement | null) => void; 94 | onMouseDown?: (event: React.MouseEvent) => void; 95 | }, 96 | AttachTouch 97 | ] { 98 | const onStart = (event: HandledEvents) => { 99 | const isTouch = "touches" in event; 100 | // if more than a single touch don't track, for now... 101 | if (isTouch && event.touches.length > 1) return; 102 | 103 | set((state, props) => { 104 | // setup mouse listeners on document to track swipe since swipe can leave container 105 | if (props.trackMouse && !isTouch) { 106 | document.addEventListener(mouseMove, onMove); 107 | document.addEventListener(mouseUp, onUp); 108 | } 109 | const { clientX, clientY } = isTouch ? event.touches[0] : event; 110 | const xy = rotateXYByAngle([clientX, clientY], props.rotationAngle); 111 | 112 | props.onTouchStartOrOnMouseDown && 113 | props.onTouchStartOrOnMouseDown({ event }); 114 | 115 | return { 116 | ...state, 117 | ...initialState, 118 | initial: xy.slice() as Vector2, 119 | xy, 120 | start: event.timeStamp || 0, 121 | }; 122 | }); 123 | }; 124 | 125 | const onMove = (event: HandledEvents) => { 126 | set((state, props) => { 127 | const isTouch = "touches" in event; 128 | // Discount a swipe if additional touches are present after 129 | // a swipe has started. 130 | if (isTouch && event.touches.length > 1) { 131 | return state; 132 | } 133 | 134 | // if swipe has exceeded duration stop tracking 135 | if (event.timeStamp - state.start > props.swipeDuration) { 136 | return state.swiping ? { ...state, swiping: false } : state; 137 | } 138 | 139 | const { clientX, clientY } = isTouch ? event.touches[0] : event; 140 | const [x, y] = rotateXYByAngle([clientX, clientY], props.rotationAngle); 141 | const deltaX = x - state.xy[0]; 142 | const deltaY = y - state.xy[1]; 143 | const absX = Math.abs(deltaX); 144 | const absY = Math.abs(deltaY); 145 | const time = (event.timeStamp || 0) - state.start; 146 | const velocity = Math.sqrt(absX * absX + absY * absY) / (time || 1); 147 | const vxvy: Vector2 = [deltaX / (time || 1), deltaY / (time || 1)]; 148 | 149 | const dir = getDirection(absX, absY, deltaX, deltaY); 150 | 151 | // if swipe is under delta and we have not started to track a swipe: skip update 152 | const delta = 153 | typeof props.delta === "number" 154 | ? props.delta 155 | : props.delta[dir.toLowerCase() as Lowercase] || 156 | defaultProps.delta; 157 | if (absX < delta && absY < delta && !state.swiping) return state; 158 | 159 | const eventData = { 160 | absX, 161 | absY, 162 | deltaX, 163 | deltaY, 164 | dir, 165 | event, 166 | first: state.first, 167 | initial: state.initial, 168 | velocity, 169 | vxvy, 170 | }; 171 | 172 | // call onSwipeStart if present and is first swipe event 173 | eventData.first && props.onSwipeStart && props.onSwipeStart(eventData); 174 | 175 | // call onSwiping if present 176 | props.onSwiping && props.onSwiping(eventData); 177 | 178 | // track if a swipe is cancelable (handler for swiping or swiped(dir) exists) 179 | // so we can call preventDefault if needed 180 | let cancelablePageSwipe = false; 181 | if ( 182 | props.onSwiping || 183 | props.onSwiped || 184 | props[`onSwiped${dir}` as keyof SwipeableDirectionCallbacks] 185 | ) { 186 | cancelablePageSwipe = true; 187 | } 188 | 189 | if ( 190 | cancelablePageSwipe && 191 | props.preventScrollOnSwipe && 192 | props.trackTouch && 193 | event.cancelable 194 | ) { 195 | event.preventDefault(); 196 | } 197 | 198 | return { 199 | ...state, 200 | // first is now always false 201 | first: false, 202 | eventData, 203 | swiping: true, 204 | }; 205 | }); 206 | }; 207 | 208 | const onEnd = (event: HandledEvents) => { 209 | set((state, props) => { 210 | let eventData: SwipeEventData | undefined; 211 | if (state.swiping && state.eventData) { 212 | // if swipe is less than duration fire swiped callbacks 213 | if (event.timeStamp - state.start < props.swipeDuration) { 214 | eventData = { ...state.eventData, event }; 215 | props.onSwiped && props.onSwiped(eventData); 216 | 217 | const onSwipedDir = 218 | props[ 219 | `onSwiped${eventData.dir}` as keyof SwipeableDirectionCallbacks 220 | ]; 221 | onSwipedDir && onSwipedDir(eventData); 222 | } 223 | } else { 224 | props.onTap && props.onTap({ event }); 225 | } 226 | 227 | props.onTouchEndOrOnMouseUp && props.onTouchEndOrOnMouseUp({ event }); 228 | 229 | return { ...state, ...initialState, eventData }; 230 | }); 231 | }; 232 | 233 | const cleanUpMouse = () => { 234 | // safe to just call removeEventListener 235 | document.removeEventListener(mouseMove, onMove); 236 | document.removeEventListener(mouseUp, onUp); 237 | }; 238 | 239 | const onUp = (e: HandledEvents) => { 240 | cleanUpMouse(); 241 | onEnd(e); 242 | }; 243 | 244 | /** 245 | * The value of passive on touchMove depends on `preventScrollOnSwipe`: 246 | * - true => { passive: false } 247 | * - false => { passive: true } // Default 248 | * 249 | * NOTE: When preventScrollOnSwipe is true, we attempt to call preventDefault to prevent scroll. 250 | * 251 | * props.touchEventOptions can also be set for all touch event listeners, 252 | * but for `touchmove` specifically when `preventScrollOnSwipe` it will 253 | * supersede and force passive to false. 254 | * 255 | */ 256 | const attachTouch: AttachTouch = (el, props) => { 257 | let cleanup = () => {}; 258 | if (el && el.addEventListener) { 259 | const baseOptions = { 260 | ...defaultProps.touchEventOptions, 261 | ...props.touchEventOptions, 262 | }; 263 | // attach touch event listeners and handlers 264 | const tls: [ 265 | typeof touchStart | typeof touchMove | typeof touchEnd, 266 | (e: HandledEvents) => void, 267 | { passive: boolean } 268 | ][] = [ 269 | [touchStart, onStart, baseOptions], 270 | // preventScrollOnSwipe option supersedes touchEventOptions.passive 271 | [ 272 | touchMove, 273 | onMove, 274 | { 275 | ...baseOptions, 276 | ...(props.preventScrollOnSwipe ? { passive: false } : {}), 277 | }, 278 | ], 279 | [touchEnd, onEnd, baseOptions], 280 | ]; 281 | tls.forEach(([e, h, o]) => el.addEventListener(e, h, o)); 282 | // return properly scoped cleanup method for removing listeners, options not required 283 | cleanup = () => tls.forEach(([e, h]) => el.removeEventListener(e, h)); 284 | } 285 | return cleanup; 286 | }; 287 | 288 | const onRef = (el: HTMLElement | null) => { 289 | // "inline" ref functions are called twice on render, once with null then again with DOM element 290 | // ignore null here 291 | if (el === null) return; 292 | set((state, props) => { 293 | // if the same DOM el as previous just return state 294 | if (state.el === el) return state; 295 | 296 | const addState: { cleanUpTouch?: () => void } = {}; 297 | // if new DOM el clean up old DOM and reset cleanUpTouch 298 | if (state.el && state.el !== el && state.cleanUpTouch) { 299 | state.cleanUpTouch(); 300 | addState.cleanUpTouch = void 0; 301 | } 302 | // only attach if we want to track touch 303 | if (props.trackTouch && el) { 304 | addState.cleanUpTouch = attachTouch(el, props); 305 | } 306 | 307 | // store event attached DOM el for comparison, clean up, and re-attachment 308 | return { ...state, el, ...addState }; 309 | }); 310 | }; 311 | 312 | // set ref callback to attach touch event listeners 313 | const output: { ref: typeof onRef; onMouseDown?: typeof onStart } = { 314 | ref: onRef, 315 | }; 316 | 317 | // if track mouse attach mouse down listener 318 | if (handlerProps.trackMouse) { 319 | output.onMouseDown = onStart; 320 | } 321 | 322 | return [output, attachTouch]; 323 | } 324 | 325 | function updateTransientState( 326 | state: SwipeableState, 327 | props: SwipeablePropsWithDefaultOptions, 328 | previousProps: SwipeablePropsWithDefaultOptions, 329 | attachTouch: AttachTouch 330 | ) { 331 | // if trackTouch is off or there is no el, then remove handlers if necessary and exit 332 | if (!props.trackTouch || !state.el) { 333 | if (state.cleanUpTouch) { 334 | state.cleanUpTouch(); 335 | } 336 | 337 | return { 338 | ...state, 339 | cleanUpTouch: undefined, 340 | }; 341 | } 342 | 343 | // trackTouch is on, so if there are no handlers attached, attach them and exit 344 | if (!state.cleanUpTouch) { 345 | return { 346 | ...state, 347 | cleanUpTouch: attachTouch(state.el, props), 348 | }; 349 | } 350 | 351 | // trackTouch is on and handlers are already attached, so if preventScrollOnSwipe changes value, 352 | // remove and reattach handlers (this is required to update the passive option when attaching 353 | // the handlers) 354 | if ( 355 | props.preventScrollOnSwipe !== previousProps.preventScrollOnSwipe || 356 | props.touchEventOptions.passive !== previousProps.touchEventOptions.passive 357 | ) { 358 | state.cleanUpTouch(); 359 | 360 | return { 361 | ...state, 362 | cleanUpTouch: attachTouch(state.el, props), 363 | }; 364 | } 365 | 366 | return state; 367 | } 368 | 369 | export function useSwipeable(options: SwipeableProps): SwipeableHandlers { 370 | const { trackMouse } = options; 371 | const transientState = React.useRef({ ...initialState }); 372 | const transientProps = React.useRef({ 373 | ...defaultProps, 374 | }); 375 | 376 | // track previous rendered props 377 | const previousProps = React.useRef({ 378 | ...transientProps.current, 379 | }); 380 | previousProps.current = { ...transientProps.current }; 381 | 382 | // update current render props & defaults 383 | transientProps.current = { 384 | ...defaultProps, 385 | ...options, 386 | }; 387 | // Force defaults for config properties 388 | let defaultKey: keyof ConfigurationOptions; 389 | for (defaultKey in defaultProps) { 390 | if (transientProps.current[defaultKey] === void 0) { 391 | (transientProps.current[defaultKey] as any) = defaultProps[defaultKey]; 392 | } 393 | } 394 | 395 | const [handlers, attachTouch] = React.useMemo( 396 | () => 397 | getHandlers( 398 | (stateSetter) => 399 | (transientState.current = stateSetter( 400 | transientState.current, 401 | transientProps.current 402 | )), 403 | { trackMouse } 404 | ), 405 | [trackMouse] 406 | ); 407 | 408 | transientState.current = updateTransientState( 409 | transientState.current, 410 | transientProps.current, 411 | previousProps.current, 412 | attachTouch 413 | ); 414 | 415 | return handlers; 416 | } 417 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export const LEFT = "Left"; 4 | export const RIGHT = "Right"; 5 | export const UP = "Up"; 6 | export const DOWN = "Down"; 7 | export type HandledEvents = React.MouseEvent | TouchEvent | MouseEvent; 8 | export type Vector2 = [number, number]; 9 | export type SwipeDirections = 10 | | typeof LEFT 11 | | typeof RIGHT 12 | | typeof UP 13 | | typeof DOWN; 14 | export interface SwipeEventData { 15 | /** 16 | * Absolute displacement of swipe in x. Math.abs(deltaX); 17 | */ 18 | absX: number; 19 | /** 20 | * Absolute displacement of swipe in y. Math.abs(deltaY); 21 | */ 22 | absY: number; 23 | /** 24 | * Displacement of swipe in x. (current.x - initial.x) 25 | */ 26 | deltaX: number; 27 | /** 28 | * Displacement of swipe in y. (current.y - initial.y) 29 | */ 30 | deltaY: number; 31 | /** 32 | * Direction of swipe - Left | Right | Up | Down 33 | */ 34 | dir: SwipeDirections; 35 | /** 36 | * Source event. 37 | */ 38 | event: HandledEvents; 39 | /** 40 | * True for the first event of a tracked swipe. 41 | */ 42 | first: boolean; 43 | /** 44 | * Location where swipe started - [x, y]. 45 | */ 46 | initial: Vector2; 47 | /** 48 | * "Absolute velocity" (speed) - √(absX^2 + absY^2) / time 49 | */ 50 | velocity: number; 51 | /** 52 | * Velocity per axis - [ deltaX/time, deltaY/time ] 53 | */ 54 | vxvy: Vector2; 55 | } 56 | 57 | export type SwipeCallback = (eventData: SwipeEventData) => void; 58 | export type TapCallback = ({ event }: { event: HandledEvents }) => void; 59 | 60 | export type SwipeableDirectionCallbacks = { 61 | /** 62 | * Called after a DOWN swipe 63 | */ 64 | onSwipedDown: SwipeCallback; 65 | /** 66 | * Called after a LEFT swipe 67 | */ 68 | onSwipedLeft: SwipeCallback; 69 | /** 70 | * Called after a RIGHT swipe 71 | */ 72 | onSwipedRight: SwipeCallback; 73 | /** 74 | * Called after a UP swipe 75 | */ 76 | onSwipedUp: SwipeCallback; 77 | }; 78 | 79 | export type SwipeableCallbacks = SwipeableDirectionCallbacks & { 80 | /** 81 | * Called at start of a tracked swipe. 82 | */ 83 | onSwipeStart: SwipeCallback; 84 | /** 85 | * Called after any swipe. 86 | */ 87 | onSwiped: SwipeCallback; 88 | /** 89 | * Called for each move event during a tracked swipe. 90 | */ 91 | onSwiping: SwipeCallback; 92 | /** 93 | * Called after a tap. A touch under the min distance, `delta`. 94 | */ 95 | onTap: TapCallback; 96 | /** 97 | * Called for `touchstart` and `mousedown`. 98 | */ 99 | onTouchStartOrOnMouseDown: TapCallback; 100 | /** 101 | * Called for `touchend` and `mouseup`. 102 | */ 103 | onTouchEndOrOnMouseUp: TapCallback; 104 | }; 105 | 106 | // Configuration Options 107 | export type ConfigurationOptionDelta = 108 | | number 109 | | { [key in Lowercase]?: number }; 110 | 111 | export interface ConfigurationOptions { 112 | /** 113 | * Min distance(px) before a swipe starts. **Default**: `10` 114 | */ 115 | delta: ConfigurationOptionDelta; 116 | /** 117 | * Prevents scroll during swipe in most cases. **Default**: `false` 118 | */ 119 | preventScrollOnSwipe: boolean; 120 | /** 121 | * Set a rotation angle. **Default**: `0` 122 | */ 123 | rotationAngle: number; 124 | /** 125 | * Track mouse input. **Default**: `false` 126 | */ 127 | trackMouse: boolean; 128 | /** 129 | * Track touch input. **Default**: `true` 130 | */ 131 | trackTouch: boolean; 132 | /** 133 | * Allowable duration of a swipe (ms). **Default**: `Infinity` 134 | */ 135 | swipeDuration: number; 136 | /** 137 | * Options for touch event listeners 138 | */ 139 | touchEventOptions: { passive: boolean }; 140 | } 141 | 142 | export type SwipeableProps = Partial; 143 | 144 | export type SwipeablePropsWithDefaultOptions = Partial & 145 | ConfigurationOptions; 146 | 147 | export interface SwipeableHandlers { 148 | ref(element: HTMLElement | null): void; 149 | onMouseDown?(event: React.MouseEvent): void; 150 | } 151 | 152 | export type SwipeableState = { 153 | cleanUpTouch?: () => void; 154 | el?: HTMLElement; 155 | eventData?: SwipeEventData; 156 | first: boolean; 157 | initial: Vector2; 158 | start: number; 159 | swiping: boolean; 160 | xy: Vector2; 161 | }; 162 | 163 | export type StateSetter = ( 164 | state: SwipeableState, 165 | props: SwipeablePropsWithDefaultOptions 166 | ) => SwipeableState; 167 | export type Setter = (stateSetter: StateSetter) => void; 168 | export type AttachTouch = ( 169 | el: HTMLElement, 170 | props: SwipeablePropsWithDefaultOptions 171 | ) => () => void; 172 | -------------------------------------------------------------------------------- /tsconfig.all.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": [ 4 | "src/**/*", 5 | "examples/**/*", 6 | "__tests__", 7 | "rollup.config.js", 8 | "docs/**/*" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "baseUrl": ".", 5 | "esModuleInterop": true, 6 | "jsx": "react", 7 | "lib": ["ESNext", "dom"], 8 | "module": "ESNext", 9 | "moduleResolution": "node", 10 | "noImplicitAny": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "noEmitOnError": true, 14 | "pretty": true, 15 | "skipLibCheck": true, 16 | "sourceMap": true, 17 | "strict": true, 18 | "target": "es2015" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": ["src/**/*"], 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": ["__tests__"], 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "emitDeclarationOnly": true, 6 | "outDir": "./es", 7 | "sourceMap": false, 8 | }, 9 | "include": ["src/**/*"], 10 | "exclude": ["node_modules"] 11 | } 12 | --------------------------------------------------------------------------------