diff --git a/src/index.ts b/src/index.ts index 2b81aa4..6d15d20 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,7 @@ import { useGestureResponder } from "react-gesture-responder"; const HIGHLIGHT_DELAY_MS = 100; const PRESS_EXPAND_PX = 20; +const LONG_PRESS_DELAY = 500 - HIGHLIGHT_DELAY_MS; type States = | "ERROR" @@ -32,7 +33,9 @@ type States = | "RESPONDER_ACTIVE_IN" | "RESPONDER_ACTIVE_OUT" | "RESPONDER_PRESSED_IN" - | "RESPONDER_PRESSED_OUT"; + | "RESPONDER_PRESSED_OUT" + | "RESPONDER_LONG_PRESSED_IN" + | "RESPONDER_LONG_PRESSED_OUT"; type Events = | "DELAY" @@ -40,7 +43,8 @@ type Events = | "RESPONDER_RELEASE" | "RESPONDER_TERMINATED" | "ENTER_PRESS_RECT" - | "LEAVE_PRESS_RECT"; + | "LEAVE_PRESS_RECT" + | "LONG_PRESS_DETECTED"; type TransitionsType = { [key in States]: TransitionType }; @@ -53,7 +57,8 @@ const transitions = { RESPONDER_RELEASE: "NOT_RESPONDER", RESPONDER_TERMINATED: "NOT_RESPONDER", ENTER_PRESS_RECT: "NOT_RESPONDER", - LEAVE_PRESS_RECT: "NOT_RESPONDER" + LEAVE_PRESS_RECT: "NOT_RESPONDER", + LONG_PRESS_DETECTED: "NOT_RESPONDER" }, RESPONDER_ACTIVE_IN: { DELAY: "RESPONDER_PRESSED_IN", @@ -61,7 +66,8 @@ const transitions = { RESPONDER_RELEASE: "NOT_RESPONDER", RESPONDER_TERMINATED: "NOT_RESPONDER", ENTER_PRESS_RECT: "RESPONDER_ACTIVE_IN", - LEAVE_PRESS_RECT: "RESPONDER_ACTIVE_OUT" + LEAVE_PRESS_RECT: "RESPONDER_ACTIVE_OUT", + LONG_PRESS_DETECTED: "ERROR" }, RESPONDER_ACTIVE_OUT: { DELAY: "RESPONDER_PRESSED_OUT", @@ -69,7 +75,8 @@ const transitions = { RESPONDER_RELEASE: "NOT_RESPONDER", RESPONDER_TERMINATED: "NOT_RESPONDER", ENTER_PRESS_RECT: "RESPONDER_ACTIVE_IN", - LEAVE_PRESS_RECT: "RESPONDER_ACTIVE_OUT" + LEAVE_PRESS_RECT: "RESPONDER_ACTIVE_OUT", + LONG_PRESS_DETECTED: "ERROR" }, RESPONDER_PRESSED_IN: { DELAY: "ERROR", @@ -77,7 +84,8 @@ const transitions = { RESPONDER_RELEASE: "NOT_RESPONDER", RESPONDER_TERMINATED: "NOT_RESPONDER", ENTER_PRESS_RECT: "RESPONDER_PRESSED_IN", - LEAVE_PRESS_RECT: "RESPONDER_PRESSED_OUT" + LEAVE_PRESS_RECT: "RESPONDER_PRESSED_OUT", + LONG_PRESS_DETECTED: "RESPONDER_LONG_PRESSED_IN" }, RESPONDER_PRESSED_OUT: { DELAY: "ERROR", @@ -85,7 +93,26 @@ const transitions = { RESPONDER_RELEASE: "NOT_RESPONDER", RESPONDER_TERMINATED: "NOT_RESPONDER", ENTER_PRESS_RECT: "RESPONDER_PRESSED_IN", - LEAVE_PRESS_RECT: "RESPONDER_PRESSED_OUT" + LEAVE_PRESS_RECT: "RESPONDER_PRESSED_OUT", + LONG_PRESS_DETECTED: "ERROR" + }, + RESPONDER_LONG_PRESSED_IN: { + DELAY: "ERROR", + RESPONDER_GRANT: "ERROR", + RESPONDER_RELEASE: "NOT_RESPONDER", + RESPONDER_TERMINATED: "NOT_RESPONDER", + ENTER_PRESS_RECT: "RESPONDER_PRESSED_IN", + LEAVE_PRESS_RECT: "RESPONDER_LONG_PRESSED_OUT", + LONG_PRESS_DETECTED: "RESPONDER_LONG_PRESSED_IN" + }, + RESPONDER_LONG_PRESSED_OUT: { + DELAY: "ERROR", + RESPONDER_GRANT: "ERROR", + RESPONDER_RELEASE: "NOT_RESPONDER", + RESPONDER_TERMINATED: "NOT_RESPONDER", + ENTER_PRESS_RECT: "RESPONDER_LONG_PRESSED_IN", + LEAVE_PRESS_RECT: "RESPONDER_LONG_PRESSED_OUT", + LONG_PRESS_DETECTED: "ERROR" }, ERROR: { DELAY: "NOT_RESPONDER", @@ -93,7 +120,8 @@ const transitions = { RESPONDER_RELEASE: "NOT_RESPONDER", RESPONDER_TERMINATED: "NOT_RESPONDER", ENTER_PRESS_RECT: "NOT_RESPONDER", - LEAVE_PRESS_RECT: "NOT_RESPONDER" + LEAVE_PRESS_RECT: "NOT_RESPONDER", + LONG_PRESS_DETECTED: "NOT_RESPONDER" } } as TransitionsType; @@ -108,6 +136,7 @@ export interface TouchableOptions { disabled: boolean; terminateOnScroll: boolean; onPress?: OnPressFunction; + onLongPress?: OnPressFunction; } const defaultOptions: TouchableOptions = { @@ -116,12 +145,14 @@ const defaultOptions: TouchableOptions = { behavior: "button", disabled: false, terminateOnScroll: true, - onPress: undefined + onPress: undefined, + onLongPress: undefined }; export function useTouchable(options: Partial = {}) { const { onPress, + onLongPress, terminateOnScroll, delay, behavior, @@ -133,6 +164,7 @@ export function useTouchable(options: Partial = {}) { const disabled = localDisabled; const ref = React.useRef(null); const delayTimer = React.useRef(); + const longDelayTimer = React.useRef(); const bounds = React.useRef(); const [hover, setHover] = React.useState(false); const [showHover, setShowHover] = React.useState(true); @@ -148,7 +180,10 @@ export function useTouchable(options: Partial = {}) { const nextState = transitions[state.current][event]; state.current = nextState; - if (nextState === "RESPONDER_PRESSED_IN") { + if ( + nextState === "RESPONDER_PRESSED_IN" || + nextState === "RESPONDER_LONG_PRESSED_IN" + ) { setActive(true); } else { setActive(false); @@ -156,6 +191,7 @@ export function useTouchable(options: Partial = {}) { if (nextState === "NOT_RESPONDER") { clearTimeout(delayTimer.current); + clearTimeout(longDelayTimer.current); } } @@ -183,6 +219,12 @@ export function useTouchable(options: Partial = {}) { } } + function emitLongPress() { + if (!disabled && onLongPress) { + onLongPress(); + } + } + function bindScroll() { if (terminateOnScroll) { document.addEventListener("scroll", onScroll, true); @@ -210,14 +252,25 @@ export function useTouchable(options: Partial = {}) { delayPressMs > 0 ? window.setTimeout(afterDelay, delayPressMs) : undefined; + if (delayPressMs === 0) { dispatch("DELAY"); } + longDelayTimer.current = window.setTimeout( + afterLongDelay, + LONG_PRESS_DELAY + ); + bindScroll(); setShowHover(false); } + function afterLongDelay() { + dispatch("LONG_PRESS_DETECTED"); + emitLongPress(); + } + // onTerminate should be disambiguated from onRelease // because it should never trigger onPress events. function onTerminate() { @@ -275,12 +328,15 @@ export function useTouchable(options: Partial = {}) { return; } + clearTimeout(longDelayTimer.current); + const { clientX, clientY } = e.touches && e.touches[0] ? e.touches[0] : e; const withinBounds = isWithinActiveBounds( clientX, clientY, bounds.current! ); + if (withinBounds) { dispatch("ENTER_PRESS_RECT"); } else { @@ -330,6 +386,7 @@ export function useTouchable(options: Partial = {}) { React.useEffect(() => { return () => { clearTimeout(delayTimer.current); + clearTimeout(longDelayTimer.current); unbindScroll(); }; }, []); diff --git a/stories/intro.stories.tsx b/stories/intro.stories.tsx index 609497e..999f9eb 100644 --- a/stories/intro.stories.tsx +++ b/stories/intro.stories.tsx @@ -11,8 +11,13 @@ function TouchableHighlight({ options = {} }) { setPressCount(pressCount + 1); } + function onLongPress() { + console.log("LONG PRESSED"); + } + const { bind, active, hover } = useTouchable({ onPress, + onLongPress, behavior: "button", ...options });