import React, {HTMLAttributes, useRef} from 'react';
import {createEventHandler} from '../../utils/dom/create-event-handler';
import {useGlobalListeners} from '@react-aria/utils';
interface PointerState {
lastPosition: {x: number; y: number};
id?: number;
started: boolean;
el?: HTMLElement;
originalTouchAction?: string;
originalUserSelect?: string;
longPressTimer?: any;
longPressTriggered?: boolean;
}
interface UsePointerEventsReturn {
domProps: HTMLAttributes;
}
export interface UsePointerEventsProps {
onMoveStart?: (e: PointerEvent, el: HTMLElement) => false | void;
onMove?: (e: PointerEvent, deltaX: number, deltaY: number) => void;
onMoveEnd?: (e: PointerEvent) => void;
onPointerDown?: (e: React.PointerEvent) => void | false;
onPointerUp?: (e: PointerEvent, el: HTMLElement) => void;
onPress?: (e: PointerEvent, el: HTMLElement) => void;
onLongPress?: (e: PointerEvent | React.PointerEvent, el: HTMLElement) => void;
preventDefault?: boolean;
stopPropagation?: boolean;
minimumMovement?: number;
}
export function usePointerEvents({
onMoveStart,
onMove,
onMoveEnd,
minimumMovement = 0,
preventDefault,
stopPropagation = true,
onPress,
onLongPress,
...props
}: UsePointerEventsProps): UsePointerEventsReturn {
const stateRef = useRef({
lastPosition: {x: 0, y: 0},
started: false,
longPressTriggered: false,
});
const state = stateRef.current;
const {addGlobalListener, removeGlobalListener} = useGlobalListeners();
const start = (e: PointerEvent) => {
if (!state.el) return;
const result = onMoveStart?.(e, state.el);
// allow user to cancel interaction
if (result === false) return;
state.originalTouchAction = state.el.style.touchAction;
state.el.style.touchAction = 'none';
state.originalUserSelect = document.documentElement.style.userSelect;
document.documentElement.style.userSelect = 'none';
state.started = true;
};
const onPointerDown = (e: React.PointerEvent) => {
if (e.button === 0 && state.id == null) {
state.started = false;
const result = props.onPointerDown?.(e);
if (result === false) return;
if (stopPropagation) {
e.stopPropagation();
}
if (preventDefault) {
e.preventDefault();
}
state.id = e.pointerId;
state.el = e.currentTarget as HTMLElement;
state.lastPosition = {x: e.clientX, y: e.clientY};
// use global listeners, so we don't have to capture pointer,
// which would prevent click events on child nodes
if (onLongPress) {
state.longPressTimer = setTimeout(() => {
onLongPress(e, state.el!);
state.longPressTriggered = true;
}, 400);
}
if (onMoveStart || onMove) {
addGlobalListener(window, 'pointermove', onPointerMove, false);
}
addGlobalListener(window, 'pointerup', onPointerUp, false);
addGlobalListener(window, 'pointercancel', onPointerUp, false);
}
};
const onPointerMove = (e: PointerEvent) => {
if (e.pointerId === state.id) {
const deltaX = e.clientX - state.lastPosition.x;
const deltaY = e.clientY - state.lastPosition.y;
if (
(Math.abs(deltaX) >= minimumMovement ||
Math.abs(deltaY) >= minimumMovement) &&
!state.started
) {
start(e);
}
if (state.started) {
onMove?.(e, deltaX, deltaY);
state.lastPosition = {x: e.clientX, y: e.clientY};
}
}
};
const onPointerUp = (e: PointerEvent) => {
if (e.pointerId === state.id) {
// cancel long press timer, if exists
if (state.longPressTimer) {
clearTimeout(state.longPressTimer);
}
const longPressTriggered = state.longPressTriggered;
state.longPressTriggered = false;
// only call onMoveEnd if we actually started moving
if (state.started) {
onMoveEnd?.(e);
}
if (state.el) {
// handle press only if event was not cancelled (via touch scroll on mobile for example)
if (e.type !== 'pointercancel') {
props.onPointerUp?.(e, state.el);
// only call onPress if pointer did not leave onPointerDown element
if (e.target && state.el.contains(e.target as HTMLElement)) {
// trigger either onPress or onLongPress
if (longPressTriggered) {
onLongPress?.(e, state.el);
} else {
onPress?.(e, state.el);
}
}
}
document.documentElement.style.userSelect =
state.originalUserSelect || '';
state.el.style.touchAction = state.originalTouchAction || '';
}
state.id = undefined;
state.started = false;
removeGlobalListener(window, 'pointermove', onPointerMove, false);
removeGlobalListener(window, 'pointerup', onPointerUp, false);
removeGlobalListener(window, 'pointercancel', onPointerUp, false);
}
};
return {
domProps: {
onPointerDown: createEventHandler(onPointerDown),
},
};
}