diff --git a/.changeset/clever-feet-camp.md b/.changeset/clever-feet-camp.md new file mode 100644 index 00000000..f454553d --- /dev/null +++ b/.changeset/clever-feet-camp.md @@ -0,0 +1,5 @@ +--- +"react-swipeable": minor +--- + +Add ability to pass a function to preventScrollOnSwipe diff --git a/__tests__/useSwipeable.spec.tsx b/__tests__/useSwipeable.spec.tsx index 648fdf5f..d4ca2c8b 100644 --- a/__tests__/useSwipeable.spec.tsx +++ b/__tests__/useSwipeable.spec.tsx @@ -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( + + ); + + 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( + + ); + + 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( + + ); + + 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( + + ); + + 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); + }); }); diff --git a/docs/docs/api.md b/docs/docs/api.md index 3066738e..cd04771d 100644 --- a/docs/docs/api.md +++ b/docs/docs/api.md @@ -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). \ No newline at end of file +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). diff --git a/src/index.ts b/src/index.ts index 2958bae4..c052ed40 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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, @@ -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 { diff --git a/src/types.ts b/src/types.ts index 7e4a213b..d2edf9d7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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` */