diff --git a/CHANGELOG.md b/CHANGELOG.md index ddea36d1..7a8dd789 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # UNRELEASED **New Features:** +- add new `swipeDuration` prop - "allowable duration of a swipe" + - A swipe lasting more than `swipeDuration`, in milliseconds, will **not** be considered a swipe. + - Feature mimicked from `use-gesture` [swipe.duration](https://use-gesture.netlify.app/docs/options/#swipeduration) + - Defaults to `Infinity` for backwards compatibility - add new `touchEventOptions` prop that can set the options for the touch event listeners - 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) - add new `onTouchStartOrOnMouseDown` prop that is called for `touchstart` and `mousedown`. Before a swipe even starts. diff --git a/README.md b/README.md index e8e0cd7e..ee393264 100644 --- a/README.md +++ b/README.md @@ -51,23 +51,35 @@ Spread `handlers` onto the element you wish to track swipes on. ```js { - delta: 10, // min distance(px) before a swipe starts. *See Notes* - preventScrollOnSwipe: false, // prevents scroll during swipe (*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. *See Notes* + preventScrollOnSwipe: false, // prevents scroll during swipe (*See Details*) + trackTouch: true, // track touch input + trackMouse: false, // track mouse input + rotationAngle: 0, // set a rotation angle + swipeDuration: Infinity, // allowable duration of a swipe (ms). *See Notes* touchEventOptions: { passive: true }, // options for touch listeners (*See Details*) } ``` -#### Delta +#### delta `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`; ```js { - delta: { top: 20, bottom: 20 } // top and bottom when ">= 20", left and right default to ">= 10" + delta: { up: 20, down: 20 } // up and down ">= 20", left and right default to ">= 10" +} +``` + +#### swipeDuration +A swipe lasting more than `swipeDuration`, in milliseconds, will **not** be considered a swipe. +- It will also **not** trigger any callbacks and the swipe event will stop being tracked +- **Defaults** to `Infinity` for backwards compatibility, a sensible duration could be something like `250` + - Feature mimicked from `use-gesture` [swipe.duration](https://use-gesture.netlify.app/docs/options/#swipeduration) + +```js +{ + swipeDuration: 250 // only swipes under 250ms will trigger callbacks } ``` diff --git a/__tests__/useSwipeable.spec.tsx b/__tests__/useSwipeable.spec.tsx index 1d5a666b..0fa04f53 100644 --- a/__tests__/useSwipeable.spec.tsx +++ b/__tests__/useSwipeable.spec.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { render, fireEvent, act } from "@testing-library/react"; +import { render, fireEvent, createEvent, act } from "@testing-library/react"; import { useSwipeable } from "../src/index"; import { LEFT, RIGHT, UP, DOWN, SwipeableProps } from "../src/types"; import { expectSwipeFuncsDir } from "./helpers"; @@ -70,17 +70,36 @@ const MU = "mouseUp"; const TS = "touchStart"; const TM = "touchMove"; const TE = "touchEnd"; +type touchTypes = typeof TS | typeof TM | typeof TE; const createClientXYObject = (x?: number, y?: number) => ({ clientX: x, clientY: y, }); +type xyObj = { x?: number; y?: number }; // Create touch event -const cte = ({ x, y }: { x?: number; y?: number }) => ({ +const cte = ({ x, y }: xyObj) => ({ touches: [createClientXYObject(x, y)], }); +// create touch event with timestamp +const cteTs = ({ + x, + y, + timeStamp, + type, + node, +}: xyObj & { timeStamp?: number; type: touchTypes; node: HTMLElement }) => { + const e = createEvent[type](node, cte({ x, y })); + if (timeStamp) { + Object.defineProperty(e, "timeStamp", { + value: timeStamp, + writable: false, + }); + } + return e; +}; // Create Mouse Event -const cme = ({ x, y }: { x?: number; y?: number }) => ({ +const cme = ({ x, y }: xyObj) => ({ ...createClientXYObject(x, y), }); @@ -232,6 +251,8 @@ describe("useSwipeable", () => { rotationAngle: undefined, trackMouse: undefined, trackTouch: undefined, + swipeDuration: undefined, + touchEventOptions: undefined, }; const { getByText } = render( @@ -401,6 +422,49 @@ describe("useSwipeable", () => { expect(onSwipeStart).toHaveBeenCalledTimes(2); }); + it("calls callbacks appropriately for swipeDuration", () => { + const onSwiped = jest.fn(); + const onSwiping = jest.fn(); + const { getByText, rerender } = render( + + ); + + const el = getByText(TESTING_TEXT); + const fE = fireEvent; + + fE(el, cteTs({ x: 100, y: 100, node: el, type: TS, timeStamp: 1000 })); + fE(el, cteTs({ x: 100, y: 125, node: el, type: TM, timeStamp: 1010 })); + fE(el, cteTs({ x: 100, y: 150, node: el, type: TM, timeStamp: 1020 })); + fE(el, cteTs({ x: 100, y: 175, node: el, type: TM, timeStamp: 1030 })); + fE(el, cteTs({ node: el, type: TE, timeStamp: 1040 })); + + expect(onSwiping).toHaveBeenCalledTimes(1); + expect(onSwiped).not.toHaveBeenCalled(); + + onSwiping.mockClear(); + onSwiped.mockClear(); + rerender( + + ); + + fE(el, cteTs({ x: 100, y: 100, node: el, type: TS, timeStamp: 1000 })); + fE(el, cteTs({ x: 100, y: 125, node: el, type: TM, timeStamp: 1010 })); + fE(el, cteTs({ x: 100, y: 150, node: el, type: TM, timeStamp: 1020 })); + fE(el, cteTs({ x: 100, y: 175, node: el, type: TM, timeStamp: 1030 })); + fE(el, cteTs({ node: el, type: TE, timeStamp: 1040 })); + + expect(onSwiping).toHaveBeenCalledTimes(3); + expect(onSwiped).toHaveBeenCalled(); + }); + it("calls preventDefault when swiping in direction with callback defined", () => { const onSwipedDown = jest.fn(); diff --git a/examples/app/FeatureTestConsole/index.tsx b/examples/app/FeatureTestConsole/index.tsx index de11f7f4..32c8c1ff 100644 --- a/examples/app/FeatureTestConsole/index.tsx +++ b/examples/app/FeatureTestConsole/index.tsx @@ -11,12 +11,14 @@ const initialState = { swipingDirection: '', swipedDirection: '', }; +const INFINITY = 'Infinity'; const initialStateSwipeable = { delta: '10', preventScrollOnSwipe: false, trackMouse: false, trackTouch: true, rotationAngle: 0, + swipeDuration: INFINITY, }; const initialStateApplied = { showOnSwipeds: false, @@ -33,6 +35,7 @@ interface IState { swipingDirection: string; swipedDirection: string; delta: string; + swipeDuration: string; preventScrollOnSwipe: boolean; trackMouse: boolean; trackTouch: boolean; @@ -130,9 +133,14 @@ export default class Main extends Component { trackTouch, trackMouse, rotationAngle, + swipeDuration, stopScrollCss, } = this.state; + const isSwipeDurationInfinity = swipeDuration === INFINITY; + const swipeDurationTextValue = isSwipeDurationInfinity ? INFINITY : swipeDuration; + const isSwipeDurationNumber = isSwipeDurationInfinity ? Infinity : !(isNaN(swipeDuration as any) || swipeDuration === ''); + const isDeltaNumber = !(isNaN(delta as any) || delta === ''); const isRotationAngleNumber = !(isNaN(rotationAngle as any) || rotationAngle === ''); const deltaNum = isDeltaNumber ? +delta : 10; @@ -170,6 +178,7 @@ export default class Main extends Component { trackTouch={trackTouch} trackMouse={trackMouse} rotationAngle={rotationAngleNum} + swipeDuration={swipeDuration} className="callout hookComponent" style={swipeableStyle}>
this.resetState()} onMouseDown={()=>this.resetState()}> @@ -245,6 +254,17 @@ export default class Main extends Component { onChange={(e)=>this.updateValue('rotationAngle', getVal(e))} value={rotationAngle}/> + + + swipeDuration: +
(ms | Infinity)
+ + + this.updateValue('swipeDuration', getVal(e))} value={swipeDurationTextValue}/> + + props.swipeDuration) { + return state.swiping ? { ...state, swiping: false } : state; + } + const { clientX, clientY } = "touches" in event ? event.touches[0] : event; const [x, y] = rotateXYByAngle([clientX, clientY], props.rotationAngle); @@ -203,14 +209,17 @@ function getHandlers( set((state, props) => { let eventData: SwipeEventData | undefined; if (state.swiping && state.eventData) { - eventData = { ...state.eventData, event }; - props.onSwiped && props.onSwiped(eventData); - - const onSwipedDir = - props[ - `onSwiped${eventData.dir}` as keyof SwipeableDirectionCallbacks - ]; - onSwipedDir && onSwipedDir(eventData); + // if swipe is less than duration fire swiped callbacks + if (event.timeStamp - state.start < props.swipeDuration) { + eventData = { ...state.eventData, event }; + props.onSwiped && props.onSwiped(eventData); + + const onSwipedDir = + props[ + `onSwiped${eventData.dir}` as keyof SwipeableDirectionCallbacks + ]; + onSwipedDir && onSwipedDir(eventData); + } } else { props.onTap && props.onTap({ event }); } @@ -374,17 +383,14 @@ export function useSwipeable(options: SwipeableProps): SwipeableHandlers { transientProps.current = { ...defaultProps, ...options, - // Force defaults for config properties - delta: options.delta === void 0 ? defaultProps.delta : options.delta, - rotationAngle: - options.rotationAngle === void 0 - ? defaultProps.rotationAngle - : options.rotationAngle, - trackTouch: - options.trackTouch === void 0 - ? defaultProps.trackTouch - : options.trackTouch, }; + // Force defaults for config properties + let defaultKey: keyof ConfigurationOptions; + for (defaultKey in defaultProps) { + if (transientProps.current[defaultKey] === void 0) { + (transientProps.current[defaultKey] as any) = defaultProps[defaultKey]; + } + } const [handlers, attachTouch] = React.useMemo( () => diff --git a/src/types.ts b/src/types.ts index 701e7af1..1978dca9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -59,8 +59,8 @@ export type TapCallback = ({ event }: { event: HandledEvents }) => void; export type SwipeableDirectionCallbacks = { /** - * Called after a DOWN swipe - */ + * Called after a DOWN swipe + */ onSwipedDown: SwipeCallback; /** * Called after a LEFT swipe @@ -82,8 +82,8 @@ export type SwipeableCallbacks = SwipeableDirectionCallbacks & { */ onSwipeStart: SwipeCallback; /** - * Called after any swipe. - */ + * Called after any swipe. + */ onSwiped: SwipeCallback; /** * Called for each move event during a tracked swipe. @@ -129,6 +129,10 @@ export interface ConfigurationOptions { * Track touch input. **Default**: `true` */ trackTouch: boolean; + /** + * Allowable duration of a swipe (ms). **Default**: `Infinity` + */ + swipeDuration: number; /** * Options for touch event listeners */