Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/clever-feet-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-swipeable": minor
---

Add ability to pass a function to preventScrollOnSwipe
80 changes: 80 additions & 0 deletions __tests__/useSwipeable.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1093,4 +1093,84 @@ describe("useSwipeable", () => {
// verify we did NOT trigger another swipe
expect(onSwiped).toHaveBeenCalledTimes(1);
});

it("calls preventDefault when passing a function that returns true", () => {
const onSwipedDown = jest.fn();
const preventScrollOnSwipe = () => true;

const { getByText, rerender } = render(
<SwipeableUsingHook
onSwipedDown={undefined}
preventScrollOnSwipe={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={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 }));
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(4);
});

it("does not preventDefault when passing a function that returns false", () => {
const onSwipedDown = jest.fn();
const preventScrollOnSwipe = () => false;

const { getByText, rerender } = render(
<SwipeableUsingHook
onSwipedDown={undefined}
preventScrollOnSwipe={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={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 }));
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(0);
});
});
5 changes: 4 additions & 1 deletion docs/docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,18 @@ Swipeable will call `e.preventDefault()` internally in an attempt to stop the br

Please experiment with the [Feature Testing Console](examples/feature-test-console) to test `preventScrollOnSwipe`.

You may also pass a function to this option which accepts `SwipeEventData` and returns a boolean. This could be useful if you only want to prevent scroll based on certain criteria.

#### passive listener details
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`.

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

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 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 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).
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).
10 changes: 8 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ function getHandlers(
defaultProps.delta;
if (absX < delta && absY < delta && !state.swiping) return state;

const eventData = {
const eventData: SwipeEventData = {
absX,
absY,
deltaX,
Expand Down Expand Up @@ -192,7 +192,13 @@ function getHandlers(
props.trackTouch &&
event.cancelable
) {
event.preventDefault();
if (typeof props.preventScrollOnSwipe === "function") {
if (props.preventScrollOnSwipe(eventData)) {
event.preventDefault();
}
} else {
event.preventDefault();
}
}

return {
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export interface ConfigurationOptions {
/**
* Prevents scroll during swipe in most cases. **Default**: `false`
*/
preventScrollOnSwipe: boolean;
preventScrollOnSwipe: boolean | ((eventData: SwipeEventData) => boolean);
/**
* Set a rotation angle. **Default**: `0`
*/
Expand Down