Skip to content

Commit

Permalink
feat(onLongPress): add distanceThreshold option (#3578)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
donaldxdonald and antfu committed Dec 5, 2023
1 parent 0ab768d commit 0e04aa4
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 1 deletion.
35 changes: 34 additions & 1 deletion packages/core/onLongPress/index.test.ts
Expand Up @@ -110,17 +110,50 @@ describe('onLongPress', () => {
expect(onLongPressCallback).toHaveBeenCalledTimes(0)
}

async function triggerCallbackWithThreshold(isRef: boolean) {
const onLongPressCallback = vi.fn()
pointerdownEvent = new PointerEvent('pointerdown', { cancelable: true, bubbles: true, clientX: 20, clientY: 20 })
const moveWithinThresholdEvent = new PointerEvent('pointermove', { cancelable: true, bubbles: true, clientX: 17, clientY: 25 })
const moveOutsideThresholdEvent = new PointerEvent('pointermove', { cancelable: true, bubbles: true, clientX: 4, clientY: 30 })
onLongPress(isRef ? element : element.value, onLongPressCallback, { distanceThreshold: 15, delay: 1000 })
// first pointer down
element.value.dispatchEvent(pointerdownEvent)

// pointer move outside threshold
await promiseTimeout(500)
element.value.dispatchEvent(moveOutsideThresholdEvent)
await promiseTimeout(500)
expect(onLongPressCallback).toHaveBeenCalledTimes(0)

// pointer up to cancel callback
element.value.dispatchEvent(pointerUpEvent)

// wait for 500ms after pointer up
await promiseTimeout(500)
expect(onLongPressCallback).toHaveBeenCalledTimes(0)

// another pointer down
element.value.dispatchEvent(pointerdownEvent)

// pointer move within threshold
await promiseTimeout(500)
element.value.dispatchEvent(moveWithinThresholdEvent)
await promiseTimeout(500)
expect(onLongPressCallback).toHaveBeenCalledTimes(1)
}

function suites(isRef: boolean) {
describe('given no options', () => {
it('should trigger longpress after 500ms', () => triggerCallback(isRef))
})

describe('given options', () => {
it('should trigger longpress after options.delay ms', () => triggerCallbackWithDelay(isRef))
it('should not tirgger longpress when child element on longpress', () => notTriggerCallbackOnChildLongPress(isRef))
it('should not trigger longpress when child element on longpress', () => notTriggerCallbackOnChildLongPress(isRef))
it('should work with once and prevent modifiers', () => workOnceAndPreventModifiers(isRef))
it('should stop propagation', () => stopPropagation(isRef))
it('should remove event listeners after being stopped', () => stopEventListeners(isRef))
it('should trigger longpress if pointer is moved', () => triggerCallbackWithThreshold(isRef))
})
}

Expand Down
36 changes: 36 additions & 0 deletions packages/core/onLongPress/index.ts
Expand Up @@ -2,8 +2,10 @@ import { computed } from 'vue-demi'
import type { MaybeElementRef } from '../unrefElement'
import { unrefElement } from '../unrefElement'
import { useEventListener } from '../useEventListener'
import type { Position } from '../types'

const DEFAULT_DELAY = 500
const DEFAULT_THRESHOLD = 10

export interface OnLongPressOptions {
/**
Expand All @@ -14,6 +16,13 @@ export interface OnLongPressOptions {
delay?: number

modifiers?: OnLongPressModifiers

/**
* Allowance of moving distance in pixels,
* The action will get canceled When moving too far from the pointerdown position.
* @default 10
*/
distanceThreshold?: number | false
}

export interface OnLongPressModifiers {
Expand All @@ -32,12 +41,14 @@ export function onLongPress(
const elementRef = computed(() => unrefElement(target))

let timeout: ReturnType<typeof setTimeout> | undefined
let posStart: Position | undefined

function clear() {
if (timeout) {
clearTimeout(timeout)
timeout = undefined
}
posStart = undefined
}

function onDown(ev: PointerEvent) {
Expand All @@ -52,19 +63,44 @@ export function onLongPress(
if (options?.modifiers?.stop)
ev.stopPropagation()

posStart = {
x: ev.x,
y: ev.y,
}
timeout = setTimeout(
() => handler(ev),
options?.delay ?? DEFAULT_DELAY,
)
}

function onMove(ev: PointerEvent) {
if (options?.modifiers?.self && ev.target !== elementRef.value)
return

if (!posStart || options?.distanceThreshold === false)
return

if (options?.modifiers?.prevent)
ev.preventDefault()

if (options?.modifiers?.stop)
ev.stopPropagation()

const dx = ev.x - posStart.x
const dy = ev.y - posStart.y
const distance = Math.sqrt(dx * dx + dy * dy)
if (distance >= (options?.distanceThreshold ?? DEFAULT_THRESHOLD))
clear()
}

const listenerOptions: AddEventListenerOptions = {
capture: options?.modifiers?.capture,
once: options?.modifiers?.once,
}

const cleanup = [
useEventListener(elementRef, 'pointerdown', onDown, listenerOptions),
useEventListener(elementRef, 'pointermove', onMove, listenerOptions),
useEventListener(elementRef, ['pointerup', 'pointerleave'], clear, listenerOptions),
]

Expand Down

0 comments on commit 0e04aa4

Please sign in to comment.