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 scroll prop #240

Merged
merged 13 commits into from
Feb 21, 2022
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
# UNRELEASED
**Breaking Changes:**
* `preventScrollOnSwipe` - "new" prop. Replaces `preventDefaultTouchmoveEvent`
* same functionality but renamed to be more explicit on its intended use
* **fixed bug** - where toggling this prop did not re-attach event listeners
* **small update** - we now only change the `passive` event listener option for `touchmove` depending on this prop
* see notes in README for more details [readme#passive-listener](https://github.com/FormidableLabs/react-swipeable#passive-listener)
**Bug fixes:**
* fix bug where directional swiped check allowed `undefined`/falsy values to set `cancelablePageSwipe`
* Thank you [@bhj](https://github.com/bhj) for the [comment](https://github.com/FormidableLabs/react-swipeable/pull/240#issuecomment-1014980025)

# v6.2.0
* `delta` prop can now be an `object` specifying different values for each direction
* [PR #260](https://github.com/formidablelabs/react-swipeable/pull/260)
Expand Down
55 changes: 37 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Spread `handlers` onto the element you wish to track swipes on.
```js
{
delta: 10, // min distance(px) before a swipe starts. *See Notes*
preventDefaultTouchmoveEvent: false, // call e.preventDefault *See Details*
preventScrollOnSwipe: false, // prevents scroll during swipe in most cases (*See Details*)
trackTouch: true, // track touch input
trackMouse: false, // track mouse input
rotationAngle: 0, // set a rotation angle
Expand Down Expand Up @@ -92,33 +92,34 @@ All Event Handlers are called with the below event data, `SwipeEventData`.
- The props contained in `handlers` are currently `ref` and `onMouseDown`
- Please spread `handlers` as the props contained in it could change as react improves event listening capabilities

### `preventDefaultTouchmoveEvent` details
### `preventScrollOnSwipe` details

This prop allows you to prevent the browser's [touchmove](https://developer.mozilla.org/en-US/docs/Web/Events/touchmove) event default action, mostly "scrolling".
This prop prevents the browser's [touchmove](https://developer.mozilla.org/en-US/docs/Web/Events/touchmove) event default action (mostly scrolling) by calling `e.preventDefault()` internally.

Use this to **stop scrolling** in the browser while a user swipes.
- You can additionally try `touch-action` css property, [see below](#how-to-use-touch-action-to-prevent-scrolling)

`e.preventDefault()` is only called when:
- `preventDefaultTouchmoveEvent: true`
- `preventScrollOnSwipe: true`
- `trackTouch: true`
- the users current swipe has an associated `onSwiping` or `onSwiped` handler/prop

Example scenario:
> If a user is swiping right with props `{ onSwipedRight: 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 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.

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

#### passive listener
With v6 we've added the passive event listener option, by default, to **internal uses** of `addEventListener`. We set the `passive` option to `false` only when `preventDefaultTouchmoveEvent` is `true`.
With v6 we've added the passive event listener option, by default, to **internal uses** of `addEventListener`. We set the `passive` option to `false` only when `preventScrollOnSwipe` is `true` and only `onTouchMove`. Other listeners will retain `passive: true`.

**When `preventDefaultTouchmoveEvent` is:**
- `true` => `el.addEventListener(event, cb, { passive: false })`
- `false` => `el.addEventListener(event, cb, { passive: true })`
**When `preventScrollOnSwipe` is:**
- `true` => `el.addEventListener('touchmove', cb, { passive: false })`
- `false` => `el.addEventListener('touchmove', cb, { passive: true })`

React's long running passive [event issue](https://github.com/facebook/react/issues/6436).
Here is more information on react's long running passive [event issue](https://github.com/facebook/react/issues/6436).

We previously had issues with chrome lighthouse performance deducting points for not having passive option set.
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.

If, however, you really **need** _all_ of the listeners to be passive (for performance reasons or otherwise), you can use the `touch-action` css property instead, [see below for an example](#how-to-use-touch-action-to-prevent-scrolling).

### Browser Support

Expand Down Expand Up @@ -170,21 +171,39 @@ const MyComponent = () => {

### How to use `touch-action` to prevent scrolling?

Sometimes you don't want the `body` of your page to scroll along with the user manipulating or swiping an item.
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.

You might try to prevent the event default action via [preventDefaultTouchmoveEvent](#preventdefaulttouchmoveevent-details), which calls `event.preventDefault()`. **But** there may be a simpler, more effective solution, which has to do with a simple CSS property.
You can prevent scrolling via [preventScrollOnSwipe](#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.

`touch-action` is a CSS property that sets how an element's region can be manipulated by a touchscreen user.
`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.

#### Static example
```js
const handlers = useSwipeable({
onSwiped: (eventData) => console.log("User Swiped!", evenData),
onSwiped: (eventData) => console.log("User Swiped!", eventData),
...config,
});
return <div {...handlers} style={{ touchAction: 'pan-y' }}> Swipe here </div>;

return <div {...handlers} style={{ touchAction: 'pan-y' }}>Swipe here</div>;
```
This explanation and example borrowed from `use-gesture`'s [wonderful docs](https://use-gesture.netlify.app/docs/extras/#touch-action).

#### Dynamic example
```js
const MySwipeableComponent = props => {
const [stopScroll, setStopScroll] = useState(false);

const handlers = useSwipeable({
onSwipeStart: () => setStopScroll(true),
onSwiped: () => setStopScroll(false)
});

return <div {...handlers} style={{ touchAction: stopScroll ? 'none' : 'auto' }}>Swipe here</div>;
};
```

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.

## License

[MIT]((./LICENSE))
Expand Down
127 changes: 117 additions & 10 deletions __tests__/useSwipeable.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,62 @@ describe("useSwipeable", () => {
expect(onTap).not.toHaveBeenCalled();
});

it("skips touch events with more than 1 touch", () => {
const swipeFuncs = getMockedSwipeFunctions();
const { getByText } = render(<SwipeableUsingHook {...swipeFuncs} />);

const touchArea = getByText(TESTING_TEXT);

const setupMultipleTouchEvent = (
touches: { x?: number; y?: number }[]
) => ({
touches: touches.map((t) => createClientXYObject(t.x, t.y)),
});

fireEvent[TS](touchArea, cte({ x: 100, y: 10 }));
fireEvent[TM](
touchArea,
setupMultipleTouchEvent([
{ x: 125, y: 0 },
{ x: 130, y: 10 },
])
);
fireEvent[TM](
touchArea,
setupMultipleTouchEvent([
{ x: 130, y: 0 },
{ x: 135, y: 10 },
])
);
fireEvent[TE](touchArea, cte({}));

fireEvent[TS](
touchArea,
setupMultipleTouchEvent([
{ x: 100, y: 0 },
{ x: 110, y: 10 },
])
);
fireEvent[TM](
touchArea,
setupMultipleTouchEvent([
{ x: 125, y: 0 },
{ x: 130, y: 10 },
])
);
fireEvent[TM](
touchArea,
setupMultipleTouchEvent([
{ x: 130, y: 0 },
{ x: 135, y: 10 },
])
);
fireEvent[TE](touchArea, cte({}));

expect(swipeFuncs.onSwiping).toHaveBeenCalledTimes(0);
expect(swipeFuncs.onSwiped).toHaveBeenCalledTimes(0);
});

it("handles mouse events with trackMouse prop and fires correct props", () => {
const swipeFuncs = getMockedSwipeFunctions();
const { getByText } = render(
Expand Down Expand Up @@ -252,18 +308,26 @@ describe("useSwipeable", () => {
expect(onSwipeStart).toHaveBeenCalledTimes(2);
});

it("calls preventDefault when swiping in direction that has a callback", () => {
it("calls preventDefault when swiping in direction with callback defined", () => {
const onSwipedDown = jest.fn();

const { getByText } = render(
<SwipeableUsingHook
onSwipedDown={onSwipedDown}
preventDefaultTouchmoveEvent
/>
const { getByText, rerender } = render(
<SwipeableUsingHook onSwipedDown={undefined} preventScrollOnSwipe />
);

const touchArea = getByText(TESTING_TEXT);

fireEvent[TS](touchArea, cte({ x: 100, y: 100 }));
fireEvent[TM](touchArea, cte({ x: 100, y: 150 }));
fireEvent[TM](touchArea, cte({ x: 100, y: 200 }));
fireEvent[TE](touchArea, cte({}));

// Validate `undefined` does not trigger defaultPrevented
expect(onSwipedDown).not.toHaveBeenCalled();
expect(defaultPrevented).toBe(0);

rerender(<SwipeableUsingHook onSwipedDown={onSwipedDown} preventScrollOnSwipe />)

fireEvent[TS](touchArea, cte({ x: 100, y: 100 }));
fireEvent[TM](touchArea, cte({ x: 100, y: 125 }));
fireEvent[TM](touchArea, cte({ x: 100, y: 150 }));
Expand Down Expand Up @@ -300,7 +364,7 @@ describe("useSwipeable", () => {
const onSwiped = jest.fn();

const { getByText, rerender } = render(
<SwipeableUsingHook onSwiping={onSwiping} preventDefaultTouchmoveEvent />
<SwipeableUsingHook onSwiping={onSwiping} preventScrollOnSwipe />
);

const touchArea = getByText(TESTING_TEXT);
Expand All @@ -312,9 +376,7 @@ describe("useSwipeable", () => {
expect(onSwiping).toHaveBeenCalled();
expect(defaultPrevented).toBe(1);

rerender(
<SwipeableUsingHook onSwiped={onSwiped} preventDefaultTouchmoveEvent />
);
rerender(<SwipeableUsingHook onSwiped={onSwiped} preventScrollOnSwipe />);

fireEvent[TS](touchArea, cte({ x: 100, y: 100 }));
fireEvent[TM](touchArea, cte({ x: 100, y: 50 }));
Expand All @@ -324,6 +386,51 @@ describe("useSwipeable", () => {
expect(defaultPrevented).toBe(2);
});

it("calls preventDefault appropriately when preventScrollOnSwipe value changes", () => {
const onSwipedDown = jest.fn();

const { getByText, rerender } = render(
<SwipeableUsingHook onSwipedDown={onSwipedDown} preventScrollOnSwipe />
);

const touchArea = getByText(TESTING_TEXT);

fireEvent[TS](touchArea, cte({ x: 100, y: 100 }));
fireEvent[TM](touchArea, cte({ x: 100, y: 125 }));
fireEvent[TM](touchArea, cte({ x: 100, y: 150 }));

// change preventScrollOnSwipe in middle of swipe
rerender(
<SwipeableUsingHook
onSwipedDown={onSwipedDown}
preventScrollOnSwipe={false}
/>
);

fireEvent[TM](touchArea, cte({ x: 100, y: 175 }));
fireEvent[TM](touchArea, cte({ x: 100, y: 200 }));
fireEvent[TE](touchArea, cte({}));

expect(onSwipedDown).toHaveBeenCalled();
expect(defaultPrevented).toBe(2);
});

it("does not fire onSwiped when under delta", () => {
const onSwiped = jest.fn();
const { getByText } = render(
<SwipeableUsingHook onSwiped={onSwiped} delta={40} />
);

const touchArea = getByText(TESTING_TEXT);

fireEvent[TS](touchArea, cte({ x: 100, y: 100 }));
fireEvent[TM](touchArea, cte({ x: 120, y: 100 }));
fireEvent[TM](touchArea, cte({ x: 130, y: 100 }));
fireEvent[TE](touchArea, cte({}));

expect(onSwiped).not.toHaveBeenCalled();
});

it("does not re-check delta when swiping already in progress", () => {
const onSwiping = jest.fn();
const onSwipedRight = jest.fn();
Expand Down
35 changes: 19 additions & 16 deletions examples/app/FeatureTestConsole/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const initialState = {
};
const initialStateSwipeable = {
delta: '10',
preventDefaultTouchmoveEvent: false,
preventScrollOnSwipe: false,
trackMouse: false,
trackTouch: true,
rotationAngle: 0,
Expand All @@ -23,10 +23,7 @@ const initialStateApplied = {
onSwipingApplied: true,
onSwipedApplied: true,
onTapApplied: true,
onSwipedLeftApplied: true,
onSwipedRightApplied: true,
onSwipedUpApplied: true,
onSwipedDownApplied: true,
stopScrollCss: false,
};

interface IState {
Expand All @@ -36,18 +33,15 @@ interface IState {
swipingDirection: string;
swipedDirection: string;
delta: string;
preventDefaultTouchmoveEvent: boolean;
preventScrollOnSwipe: boolean;
trackMouse: boolean;
trackTouch: boolean;
rotationAngle: number | string;
showOnSwipeds: boolean;
onSwipingApplied: boolean;
onSwipedApplied: boolean;
onTapApplied: boolean;
onSwipedLeftApplied: boolean;
onSwipedRightApplied: boolean;
onSwipedUpApplied: boolean;
onSwipedDownApplied: boolean;
stopScrollCss: boolean;
}

export default class Main extends Component<any, IState> {
Expand Down Expand Up @@ -132,18 +126,22 @@ export default class Main extends Component<any, IState> {
onSwipingApplied,
onSwipedApplied,
onTapApplied,
preventDefaultTouchmoveEvent,
preventScrollOnSwipe,
trackTouch,
trackMouse,
rotationAngle,
stopScrollCss,
} = this.state;

const isDeltaNumber = !(isNaN(delta as any) || delta === '');
const isRotationAngleNumber = !(isNaN(rotationAngle as any) || rotationAngle === '');
const deltaNum = isDeltaNumber ? +delta : 10;
const rotationAngleNum = isRotationAngleNumber ? +rotationAngle : 0;

const swipeableStyle = {fontSize: "0.75rem"};
const swipeableStyle = {
fontSize: '0.75rem',
touchAction: stopScrollCss ? 'none' : 'auto',
};

const boundSwipes = getBoundSwipes(this);
let swipeableDirProps: any = {};
Expand All @@ -168,7 +166,7 @@ export default class Main extends Component<any, IState> {
{...boundSwipes}
{...swipeableDirProps}
delta={deltaNum}
preventDefaultTouchmoveEvent={preventDefaultTouchmoveEvent}
preventScrollOnSwipe={preventScrollOnSwipe}
trackTouch={trackTouch}
trackMouse={trackMouse}
rotationAngle={rotationAngleNum}
Expand Down Expand Up @@ -248,8 +246,8 @@ export default class Main extends Component<any, IState> {
</td>
</tr>
<RowSimpleCheckbox
value={preventDefaultTouchmoveEvent}
name="preventDefaultTouchmoveEvent"
value={preventScrollOnSwipe}
name="preventScrollOnSwipe"
onChange={this.updateValue}
/>
<RowSimpleCheckbox
Expand All @@ -266,7 +264,12 @@ export default class Main extends Component<any, IState> {
</table>
<table style={{width: "100%"}}>
<tbody>

<RowSimpleCheckbox
value={stopScrollCss}
name="stopScrollCss"
displayText="Prevent scroll via CSS (touch-action)"
onChange={this.updateValue}
/>
</tbody>
</table>
<button type="button" className="tiny button expanded" onClick={()=>this.resetState(true)}>Reset All Options</button>
Expand Down
2 changes: 1 addition & 1 deletion examples/app/SimpleCarousel/Carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const Carousel: FunctionComponent = (props) => {
const handlers = useSwipeable({
onSwipedLeft: () => slide(NEXT),
onSwipedRight: () => slide(PREV),
preventDefaultTouchmoveEvent: true,
preventScrollOnSwipe: true,
trackMouse: true
});

Expand Down
Loading