Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent vertical scrolling while swiping on Safari iOS #129

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 37 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
React Swipeable
=========================
# React Swipeable

React swipe and touch event handler component & hook

Expand All @@ -10,33 +9,43 @@ React swipe and touch event handler component & hook
[Github Pages Demo](https://dogfessional.github.io/react-swipeable/)

### Version 5 released!

[React hooks](https://reactjs.org/docs/hooks-reference.html) have been released with [16.8.0](https://reactjs.org/blog/2019/02/06/react-v16.8.0.html) 🎉

v5 of `react-swipeable` includes a hook, `useSwipeable`, that provides the same great functionality as `<Swipeable>`. See the `useSwipeable` hook in action with this [codesandbox](https://codesandbox.io/s/lrk6955l79?module=%2Fsrc%2FCarousel.js).

The component is still included and migration to v5 is straightforward. Please see the [migration doc](./migration.md) for more details including more info on the simplified api.

### Installation

```
$ npm install react-swipeable
```

### Api

Use React-hooks or a Component and set your swipe(d) handlers.

```js
import { useSwipeable, Swipeable } from 'react-swipeable'
```

#### Hook

```jsx
const handlers = useSwipeable({ onSwiped: (eventData) => eventHandler, ...config })
return (<div {...handlers}> You can swipe here </div>)
const handlers = useSwipeable({
onSwiped: eventData => eventHandler,
...config
})
return <div {...handlers}> You can swipe here </div>
```

Hook use requires **react >= 16.8.0**.

#### Component

```jsx
<Swipeable onSwiped={(eventData) => eventHandler} {...config} >
<Swipeable onSwiped={eventData => eventHandler} {...config}>
You can swipe here!
</Swipeable>
```
Expand All @@ -59,7 +68,9 @@ The Component `<Swipeable>` uses a `<div>` by default under the hood to attach e
```

### Event data

All Event Handlers are called with the below event data.

```
{
event, // source event
Expand All @@ -76,11 +87,12 @@ All Event Handlers are called with the below event data.

```
{
delta: 10, // min distance(px) before a swipe starts
preventDefaultTouchmoveEvent: false, // preventDefault on touchmove, *See Details*
trackTouch: true, // track touch input
trackMouse: false, // track mouse input
rotationAngle: 0, // set a rotation angle
delta: 10, // min distance(px) before a swipe starts
preventDefaultTouchmoveEvent: false, // preventDefault on touchmove, *See Details*
preventScrollOnHorizontalSwipe: false, // prevents scrolling on horizontal swipe *See Details*
trackTouch: true, // track touch input
trackMouse: false, // track mouse input
rotationAngle: 0, // set a rotation angle

touchHandlerOption: { // overwrite touch handler 3rd argument
passive: true|false // defaults opposite of preventDefaultTouchmoveEvent *See Details*
Expand All @@ -102,29 +114,36 @@ All Event Handlers are called with the below event data.
### preventDefaultTouchmoveEvent Details

**`preventDefaultTouchmoveEvent`** prevents the browser's [touchmove](https://developer.mozilla.org/en-US/docs/Web/Events/touchmove) event. Use this to stop the browser from scrolling while a user swipes.
* `e.preventDefault()` is only called when:
* `preventDefaultTouchmoveEvent: true`
* `trackTouch: true`
* the users current swipe has an associated `onSwiping` or `onSwiped` handler/prop
* if `preventDefaultTouchmoveEvent: true` then `touchHandlerOption` defaults to `{ passive: false }`
* if `preventDefaultTouchmoveEvent: false` then `touchHandlerOption` defaults to `{ passive: true }`

- `e.preventDefault()` is only called when:
- `preventDefaultTouchmoveEvent: true`
- `trackTouch: true`
- the users current swipe has an associated `onSwiping` or `onSwiped` handler/prop
- if `preventDefaultTouchmoveEvent: true` then `touchHandlerOption` defaults to `{ passive: false }`
- if `preventDefaultTouchmoveEvent: false` then `touchHandlerOption` defaults to `{ passive: true }`

Example:
* If a user is swiping right with `<Swipable onSwipedRight={this.userSwipedRight} preventDefaultTouchmoveEvent={true} >` then `e.preventDefault()` will be called, but if the user was swiping left then `e.preventDefault()` would **not** be called.

- If a user is swiping right with `<Swipable onSwipedRight={this.userSwipedRight} preventDefaultTouchmoveEvent={true} >` then `e.preventDefault()` will be called, but if the user was swiping left then `e.preventDefault()` would **not** be called.

Please experiment with the [example](http://dogfessional.github.io/react-swipeable/) to test `preventDefaultTouchmoveEvent`.

#### Older browser support

If you need to support older browsers that do not have support for `{passive: false}` `addEventListener` 3rd argument, we recommend using [detect-passive-events](https://www.npmjs.com/package/detect-passive-events) package to determine the `touchHandlerOption` prop value.

### preventScrollOnHorizontalSwipe Details

**`preventScrollOnHorizontalSwipe`** only allows the `onSwiping` function to be called when the initial swipe direction is either _left_ or _right_. It prevents the vertical scrolling.
If the initial scroll direction is _up_ or _down_, the `onSwiping` function will not be called.

## Development

Initial set up, with `node 10+`, run `npm install`.

Make changes/updates to the `src/index.js` file.

***Please add tests if PR adds/changes functionality.***
**_Please add tests if PR adds/changes functionality._**
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok prettier changed it to italic automatically. I can undo this of course

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think i may need to exclude readme from prettier, lol


#### Verify updates with the examples

Expand Down
8 changes: 8 additions & 0 deletions examples/app/FeatureTestConsole.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const initialState = {
const initialStateSwipeable = {
delta: '10',
preventDefaultTouchmoveEvent: false,
preventScrollOnHorizontalSwipe: false,
trackMouse: false,
trackTouch: true,
rotationAngle: 0,
Expand Down Expand Up @@ -104,6 +105,7 @@ export default class Main extends Component {
onSwipedApplied,
persistEvent,
preventDefaultTouchmoveEvent,
preventScrollOnHorizontalSwipe,
trackTouch,
trackMouse,
rotationAngle,
Expand Down Expand Up @@ -134,6 +136,7 @@ export default class Main extends Component {
{...swipeableDirProps}
delta={deltaNum}
preventDefaultTouchmoveEvent={preventDefaultTouchmoveEvent}
preventScrollOnHorizontalSwipe={preventScrollOnHorizontalSwipe}
trackTouch={trackTouch}
trackMouse={trackMouse}
rotationAngle={rotationAngleNum}
Expand Down Expand Up @@ -209,6 +212,11 @@ export default class Main extends Component {
name="preventDefaultTouchmoveEvent"
onChange={this.updateValue}
/>
<RowSimpleCheckbox
value={preventScrollOnHorizontalSwipe}
name="preventScrollOnHorizontalSwipe"
onChange={this.updateValue}
/>
<RowSimpleCheckbox
value={trackTouch}
name="trackTouch"
Expand Down
57 changes: 57 additions & 0 deletions src/__tests__/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,63 @@ function setupGetMountedComponent(type) {
wrapper.unmount()
})

it('should not call onSwiping when initial scroll direction is up or down and preventScrollOnHorizontalSwipe is set', () => {
const onSwiping = jest.fn()
const wrapper = getMountedComponent({
onSwiping,
preventScrollOnHorizontalSwipe: true
})

const touchHere = wrapper.find('span')
touchHere.simulate('touchStart', cte({ x: 100, y: 100 }))

eventListenerMap.touchmove(cte({ x: 100, y: 50 }))
eventListenerMap.touchmove(cte({ x: 100, y: 150 }))
eventListenerMap.touchend({})

expect(onSwiping).not.toHaveBeenCalled()

wrapper.unmount()
})

it('should call onSwiping when initial scroll direction is left or right and preventScrollOnHorizontalSwipe is set', () => {
const onSwiping = jest.fn()
const wrapper = getMountedComponent({
onSwiping,
preventScrollOnHorizontalSwipe: true
})

const touchHere = wrapper.find('span')
touchHere.simulate('touchStart', cte({ x: 100, y: 100 }))

eventListenerMap.touchmove(cte({ x: 50, y: 100 }))
eventListenerMap.touchmove(cte({ x: 150, y: 100 }))
eventListenerMap.touchend({})

expect(onSwiping).toHaveBeenCalled()

wrapper.unmount()
})

it('should call onSwiping when initial scroll direction is up or down and preventScrollOnHorizontalSwipe is not set', () => {
const onSwiping = jest.fn()
const wrapper = getMountedComponent({
onSwiping,
preventScrollOnHorizontalSwipe: false
})

const touchHere = wrapper.find('span')
touchHere.simulate('touchStart', cte({ x: 100, y: 100 }))

eventListenerMap.touchmove(cte({ x: 100, y: 50 }))
eventListenerMap.touchmove(cte({ x: 100, y: 150 }))
eventListenerMap.touchend({})

expect(onSwiping).toHaveBeenCalled()

wrapper.unmount()
})

it('does not re-check delta when swiping already in progress', () => {
const onSwiping = jest.fn()
const onSwipedRight = jest.fn()
Expand Down
45 changes: 43 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'

const defaultProps = {
preventDefaultTouchmoveEvent: false,
preventScrollOnHorizontalSwipe: false,
delta: 10,
rotationAngle: 0,
trackMouse: false,
Expand All @@ -15,6 +16,7 @@ const initialState = {
lastEventData: undefined,
start: undefined
}
let initialSwipeDirection = ''
export const LEFT = 'Left'
export const RIGHT = 'Right'
export const UP = 'Up'
Expand Down Expand Up @@ -83,14 +85,45 @@ function getHandlers(set, props) {
const time = (event.timeStamp || 0) - state.start
const velocity = Math.sqrt(absX * absX + absY * absY) / (time || 1)

if (!initialSwipeDirection) {
initialSwipeDirection = getDirection(absX, absY, deltaX, deltaY)

if (
props.preventScrollOnHorizontalSwipe &&
(initialSwipeDirection === LEFT || initialSwipeDirection === RIGHT)
) {
// disable scrolling
document.ontouchmove = e => {
e.preventDefault()
}
}
}

// if swipe is under delta and we have not started to track a swipe: skip update
if (absX < props.delta && absY < props.delta && !state.swiping)
return state

const dir = getDirection(absX, absY, deltaX, deltaY)
const eventData = { event, absX, absY, deltaX, deltaY, velocity, dir }
const eventData = {
event,
absX,
absY,
deltaX,
deltaY,
velocity,
dir
}

props.onSwiping && props.onSwiping(eventData)
// if case: call onSwiping if preventScrollOnHorizontalSwipe is set and if the initial swipe direction is left or right
// else if case: call onSwiping if preventScrollOnHorizontalSwipe is not set
if (
props.preventScrollOnHorizontalSwipe &&
(initialSwipeDirection === LEFT || initialSwipeDirection === RIGHT)
) {
props.onSwiping && props.onSwiping(eventData)
} else if (!props.preventScrollOnHorizontalSwipe) {
props.onSwiping && props.onSwiping(eventData)
}

// track if a swipe is cancelable(handler for swiping or swiped(dir) exists)
// so we can call preventDefault if needed
Expand Down Expand Up @@ -150,6 +183,13 @@ function getHandlers(set, props) {
}

const onUp = e => {
initialSwipeDirection = ''

// enable scrolling again
document.ontouchmove = () => {
return true
}

stop()
onEnd(e)
}
Expand Down Expand Up @@ -186,6 +226,7 @@ export class Swipeable extends React.PureComponent {
onSwipedLeft: PropTypes.func,
delta: PropTypes.number,
preventDefaultTouchmoveEvent: PropTypes.bool,
preventScrollOnHorizontalSwipe: PropTypes.bool,
nodeName: PropTypes.string,
trackMouse: PropTypes.bool,
trackTouch: PropTypes.bool,
Expand Down