diff --git a/packages/hooks/src/useLongPress/__tests__/index.test.ts b/packages/hooks/src/useLongPress/__tests__/index.test.ts index 4084346e72..617a16c37d 100644 --- a/packages/hooks/src/useLongPress/__tests__/index.test.ts +++ b/packages/hooks/src/useLongPress/__tests__/index.test.ts @@ -6,7 +6,8 @@ const mockCallback = jest.fn(); const mockClickCallback = jest.fn(); const mockLongPressEndCallback = jest.fn(); -let events = {}; +let events: Record void> = {}; + const mockTarget = { addEventListener: jest.fn((event, callback) => { events[event] = callback; @@ -14,6 +15,24 @@ const mockTarget = { removeEventListener: jest.fn((event) => { Reflect.deleteProperty(events, event); }), + setPointerCapture: jest.fn(), +}; + +// 模拟 PointerEvent +const createPointerEvent = ( + type: string, + pointerId = 1, + clientX = 0, + clientY = 0, +): PointerEvent => { + return { + type, + pointerId, + clientX, + clientY, + preventDefault: jest.fn(), + stopPropagation: jest.fn(), + } as unknown as PointerEvent; }; const setup = (onLongPress: any, target, options?: Options) => @@ -22,6 +41,7 @@ const setup = (onLongPress: any, target, options?: Options) => describe('useLongPress', () => { beforeEach(() => { jest.useFakeTimers(); + jest.clearAllMocks(); }); afterEach(() => { @@ -35,9 +55,16 @@ describe('useLongPress', () => { onLongPressEnd: mockLongPressEndCallback, }); expect(mockTarget.addEventListener).toBeCalled(); - events['mousedown'](); + + const pointerDownEvent = createPointerEvent('pointerdown'); + events.pointerdown(pointerDownEvent); + expect(mockTarget.setPointerCapture).toBeCalledWith(pointerDownEvent.pointerId); + jest.advanceTimersByTime(350); - events['mouseleave'](); + + const pointerCancelEvent = createPointerEvent('pointercancel', pointerDownEvent.pointerId); + events.pointercancel(pointerCancelEvent); + expect(mockCallback).toBeCalledTimes(1); expect(mockLongPressEndCallback).toBeCalledTimes(1); expect(mockClickCallback).toBeCalledTimes(0); @@ -49,10 +76,19 @@ describe('useLongPress', () => { onLongPressEnd: mockLongPressEndCallback, }); expect(mockTarget.addEventListener).toBeCalled(); - events['mousedown'](); - events['mouseup'](); - events['mousedown'](); - events['mouseup'](); + + const pointerDown1 = createPointerEvent('pointerdown', 1); + events.pointerdown(pointerDown1); + + const pointerUp1 = createPointerEvent('pointerup', 1); + events.pointerup(pointerUp1); + + const pointerDown2 = createPointerEvent('pointerdown', 2); + events.pointerdown(pointerDown2); + + const pointerUp2 = createPointerEvent('pointerup', 2); + events.pointerup(pointerUp2); + expect(mockCallback).toBeCalledTimes(0); expect(mockLongPressEndCallback).toBeCalledTimes(0); expect(mockClickCallback).toBeCalledTimes(2); @@ -64,11 +100,20 @@ describe('useLongPress', () => { onLongPressEnd: mockLongPressEndCallback, }); expect(mockTarget.addEventListener).toBeCalled(); - events['mousedown'](); + + const longPressDown = createPointerEvent('pointerdown', 1); + events.pointerdown(longPressDown); jest.advanceTimersByTime(350); - events['mouseup'](); - events['mousedown'](); - events['mouseup'](); + + const longPressUp = createPointerEvent('pointerup', 1); + events.pointerup(longPressUp); + + const clickDown = createPointerEvent('pointerdown', 2); + events.pointerdown(clickDown); + + const clickUp = createPointerEvent('pointerup', 2); + events.pointerup(clickUp); + expect(mockCallback).toBeCalledTimes(1); expect(mockLongPressEndCallback).toBeCalledTimes(1); expect(mockClickCallback).toBeCalledTimes(1); @@ -81,19 +126,41 @@ describe('useLongPress', () => { y: 20, }, }); - expect(events['mousemove']).toBeDefined(); - events['mousedown'](new MouseEvent('mousedown')); - events['mousemove']( - new MouseEvent('mousemove', { - clientX: 40, - clientY: 10, - }), - ); + expect(events.pointermove).toBeDefined(); + + const pointerDown = createPointerEvent('pointerdown', 1, 0, 0); + events.pointerdown(pointerDown); + + const pointerMove = createPointerEvent('pointermove', 1, 40, 10); + events.pointermove(pointerMove); + jest.advanceTimersByTime(320); expect(mockCallback).not.toBeCalled(); unmount(); - expect(events['mousemove']).toBeUndefined(); + expect(events.pointermove).toBeUndefined(); + }); + + it('should handle multiple pointer interactions correctly', () => { + setup(mockCallback, mockTarget); + + const pointer1Down = createPointerEvent('pointerdown', 1); + events.pointerdown(pointer1Down); + + const pointer2Down = createPointerEvent('pointerdown', 2); + events.pointerdown(pointer2Down); + + jest.advanceTimersByTime(350); + + const pointer2Up = createPointerEvent('pointerup', 2); + events.pointerup(pointer2Up); + + const pointer1Up = createPointerEvent('pointerup', 1); + events.pointerup(pointer1Up); + + expect(mockCallback).toBeCalledTimes(1); + expect(mockTarget.setPointerCapture).toBeCalledWith(1); + expect(mockTarget.setPointerCapture).toBeCalledTimes(1); }); it(`should not work when target don't support addEventListener method`, () => { @@ -103,7 +170,7 @@ describe('useLongPress', () => { }, }); - setup(() => {}, mockTarget); + setup(() => { }, mockTarget); expect(Object.keys(events)).toHaveLength(0); }); }); diff --git a/packages/hooks/src/useLongPress/index.ts b/packages/hooks/src/useLongPress/index.ts index 38d35620e1..7ebc18a9a6 100644 --- a/packages/hooks/src/useLongPress/index.ts +++ b/packages/hooks/src/useLongPress/index.ts @@ -4,7 +4,7 @@ import type { BasicTarget } from '../utils/domTarget'; import { getTargetElement } from '../utils/domTarget'; import useEffectWithTarget from '../utils/useEffectWithTarget'; -type EventType = MouseEvent | TouchEvent; +type EventType = PointerEvent; export interface Options { delay?: number; moveThreshold?: { x?: number; y?: number }; @@ -25,8 +25,8 @@ function useLongPress( const isTriggeredRef = useRef(false); const pervPositionRef = useRef({ x: 0, y: 0 }); - const mousePressed = useRef(false); - const touchPressed = useRef(false); + const isPressed = useRef(false); + const pointerId = useRef(null); const hasMoveThreshold = !!( (moveThreshold?.x && moveThreshold.x > 0) || (moveThreshold?.y && moveThreshold.y > 0) @@ -40,9 +40,8 @@ function useLongPress( } const overThreshold = (event: EventType) => { - const { clientX, clientY } = getClientPosition(event); - const offsetX = Math.abs(clientX - pervPositionRef.current.x); - const offsetY = Math.abs(clientY - pervPositionRef.current.y); + const offsetX = Math.abs(event.clientX - pervPositionRef.current.x); + const offsetY = Math.abs(event.clientY - pervPositionRef.current.y); return !!( (moveThreshold?.x && offsetX > moveThreshold.x) || @@ -50,23 +49,6 @@ function useLongPress( ); }; - function getClientPosition(event: EventType) { - if ('TouchEvent' in window && event instanceof TouchEvent) { - return { - clientX: event.touches[0].clientX, - clientY: event.touches[0].clientY, - }; - } - if (event instanceof MouseEvent) { - return { - clientX: event.clientX, - clientY: event.clientY, - }; - } - - return { clientX: 0, clientY: 0 }; - } - const createTimer = (event: EventType) => { timerRef.current = setTimeout(() => { onLongPressRef.current(event); @@ -74,46 +56,39 @@ function useLongPress( }, delay); }; - const onTouchStart = (event: TouchEvent) => { - if (touchPressed.current) { - return; - } - touchPressed.current = true; + const onPointerDown = (event: PointerEvent) => { + if (isPressed.current) return; - if (hasMoveThreshold) { - const { clientX, clientY } = getClientPosition(event); - pervPositionRef.current.x = clientX; - pervPositionRef.current.y = clientY; - } - createTimer(event); - }; - const onMouseDown = (event: MouseEvent) => { - if ((event as any)?.sourceCapabilities?.firesTouchEvents) { - return; - } + isPressed.current = true; + pointerId.current = event.pointerId; - mousePressed.current = true; + // 捕获指针以确保即使指针移出元素也能接收到事件 + targetElement.setPointerCapture(event.pointerId); if (hasMoveThreshold) { pervPositionRef.current.x = event.clientX; pervPositionRef.current.y = event.clientY; } + createTimer(event); }; - const onMove = (event: EventType) => { + const onPointerMove = (event: PointerEvent) => { + if (!isPressed.current || event.pointerId !== pointerId.current) return; + if (timerRef.current && overThreshold(event)) { clearTimeout(timerRef.current); timerRef.current = undefined; } }; - const onTouchEnd = (event: TouchEvent) => { - if (!touchPressed.current) { - return; - } - touchPressed.current = false; + + const onPointerUp = (event: PointerEvent) => { + if (!isPressed.current || event.pointerId !== pointerId.current) return; + + isPressed.current = false; + pointerId.current = null; if (timerRef.current) { clearTimeout(timerRef.current); @@ -125,56 +100,35 @@ function useLongPress( } else if (onClickRef.current) { onClickRef.current(event); } + isTriggeredRef.current = false; }; - const onMouseUp = (event: MouseEvent) => { - if ((event as any)?.sourceCapabilities?.firesTouchEvents) { - return; - } - if (!mousePressed.current) { - return; - } - mousePressed.current = false; + const onPointerCancel = (event: PointerEvent) => { + if (!isPressed.current || event.pointerId !== pointerId.current) return; - if (timerRef.current) { - clearTimeout(timerRef.current); - timerRef.current = undefined; - } + isPressed.current = false; + pointerId.current = null; - if (isTriggeredRef.current) { - onLongPressEndRef.current?.(event); - } else if (onClickRef.current) { - onClickRef.current(event); - } - isTriggeredRef.current = false; - }; - const onMouseLeave = (event: MouseEvent) => { - if (!mousePressed.current) { - return; - } - mousePressed.current = false; if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = undefined; } + if (isTriggeredRef.current) { onLongPressEndRef.current?.(event); isTriggeredRef.current = false; } }; - targetElement.addEventListener('mousedown', onMouseDown); - targetElement.addEventListener('mouseup', onMouseUp); - targetElement.addEventListener('mouseleave', onMouseLeave); - targetElement.addEventListener('touchstart', onTouchStart); - targetElement.addEventListener('touchend', onTouchEnd); + targetElement.addEventListener('pointerdown', onPointerDown); + targetElement.addEventListener('pointerup', onPointerUp); + targetElement.addEventListener('pointercancel', onPointerCancel); if (hasMoveThreshold) { - targetElement.addEventListener('mousemove', onMove); - targetElement.addEventListener('touchmove', onMove); + targetElement.addEventListener('pointermove', onPointerMove); } return () => { @@ -183,15 +137,12 @@ function useLongPress( isTriggeredRef.current = false; } - targetElement.removeEventListener('mousedown', onMouseDown); - targetElement.removeEventListener('mouseup', onMouseUp); - targetElement.removeEventListener('mouseleave', onMouseLeave); - targetElement.removeEventListener('touchstart', onTouchStart); - targetElement.removeEventListener('touchend', onTouchEnd); + targetElement.removeEventListener('pointerdown', onPointerDown); + targetElement.removeEventListener('pointerup', onPointerUp); + targetElement.removeEventListener('pointercancel', onPointerCancel); if (hasMoveThreshold) { - targetElement.removeEventListener('mousemove', onMove); - targetElement.removeEventListener('touchmove', onMove); + targetElement.removeEventListener('pointermove', onPointerMove); } }; }, @@ -200,4 +151,4 @@ function useLongPress( ); } -export default useLongPress; +export default useLongPress; \ No newline at end of file