Skip to content

Commit 7255727

Browse files
committed
refactor: useLongPress hooks 内部实现使用 pointer 事件替代 mouse/touch 事件 && 测试用例重写
1 parent 61ac006 commit 7255727

File tree

2 files changed

+135
-99
lines changed

2 files changed

+135
-99
lines changed

packages/hooks/src/useLongPress/__tests__/index.test.ts

Lines changed: 97 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,29 @@ const mockCallback = jest.fn();
66
const mockClickCallback = jest.fn();
77
const mockLongPressEndCallback = jest.fn();
88

9-
let events = {};
9+
// 使用正确的类型定义events对象
10+
let events: Record<string, (event?: any) => void> = {};
11+
1012
const mockTarget = {
1113
addEventListener: jest.fn((event, callback) => {
1214
events[event] = callback;
1315
}),
1416
removeEventListener: jest.fn((event) => {
1517
Reflect.deleteProperty(events, event);
1618
}),
19+
setPointerCapture: jest.fn(),
20+
};
21+
22+
// 模拟 PointerEvent
23+
const createPointerEvent = (type: string, pointerId = 1, clientX = 0, clientY = 0): PointerEvent => {
24+
return {
25+
type,
26+
pointerId,
27+
clientX,
28+
clientY,
29+
preventDefault: jest.fn(),
30+
stopPropagation: jest.fn(),
31+
} as unknown as PointerEvent;
1732
};
1833

1934
const setup = (onLongPress: any, target, options?: Options) =>
@@ -22,6 +37,7 @@ const setup = (onLongPress: any, target, options?: Options) =>
2237
describe('useLongPress', () => {
2338
beforeEach(() => {
2439
jest.useFakeTimers();
40+
jest.clearAllMocks();
2541
});
2642

2743
afterEach(() => {
@@ -35,9 +51,19 @@ describe('useLongPress', () => {
3551
onLongPressEnd: mockLongPressEndCallback,
3652
});
3753
expect(mockTarget.addEventListener).toBeCalled();
38-
events['mousedown']();
54+
55+
// 模拟 pointerdown 事件
56+
const pointerDownEvent = createPointerEvent('pointerdown');
57+
events.pointerdown(pointerDownEvent);
58+
expect(mockTarget.setPointerCapture).toBeCalledWith(pointerDownEvent.pointerId);
59+
60+
// 延时触发长按
3961
jest.advanceTimersByTime(350);
40-
events['mouseleave']();
62+
63+
// 模拟 pointercancel 事件
64+
const pointerCancelEvent = createPointerEvent('pointercancel', pointerDownEvent.pointerId);
65+
events.pointercancel(pointerCancelEvent);
66+
4167
expect(mockCallback).toBeCalledTimes(1);
4268
expect(mockLongPressEndCallback).toBeCalledTimes(1);
4369
expect(mockClickCallback).toBeCalledTimes(0);
@@ -49,10 +75,21 @@ describe('useLongPress', () => {
4975
onLongPressEnd: mockLongPressEndCallback,
5076
});
5177
expect(mockTarget.addEventListener).toBeCalled();
52-
events['mousedown']();
53-
events['mouseup']();
54-
events['mousedown']();
55-
events['mouseup']();
78+
79+
// 第一次点击
80+
const pointerDown1 = createPointerEvent('pointerdown', 1);
81+
events.pointerdown(pointerDown1);
82+
83+
const pointerUp1 = createPointerEvent('pointerup', 1);
84+
events.pointerup(pointerUp1);
85+
86+
// 第二次点击
87+
const pointerDown2 = createPointerEvent('pointerdown', 2);
88+
events.pointerdown(pointerDown2);
89+
90+
const pointerUp2 = createPointerEvent('pointerup', 2);
91+
events.pointerup(pointerUp2);
92+
5693
expect(mockCallback).toBeCalledTimes(0);
5794
expect(mockLongPressEndCallback).toBeCalledTimes(0);
5895
expect(mockClickCallback).toBeCalledTimes(2);
@@ -64,11 +101,22 @@ describe('useLongPress', () => {
64101
onLongPressEnd: mockLongPressEndCallback,
65102
});
66103
expect(mockTarget.addEventListener).toBeCalled();
67-
events['mousedown']();
104+
105+
// 长按
106+
const longPressDown = createPointerEvent('pointerdown', 1);
107+
events.pointerdown(longPressDown);
68108
jest.advanceTimersByTime(350);
69-
events['mouseup']();
70-
events['mousedown']();
71-
events['mouseup']();
109+
110+
const longPressUp = createPointerEvent('pointerup', 1);
111+
events.pointerup(longPressUp);
112+
113+
// 点击
114+
const clickDown = createPointerEvent('pointerdown', 2);
115+
events.pointerdown(clickDown);
116+
117+
const clickUp = createPointerEvent('pointerup', 2);
118+
events.pointerup(clickUp);
119+
72120
expect(mockCallback).toBeCalledTimes(1);
73121
expect(mockLongPressEndCallback).toBeCalledTimes(1);
74122
expect(mockClickCallback).toBeCalledTimes(1);
@@ -81,19 +129,47 @@ describe('useLongPress', () => {
81129
y: 20,
82130
},
83131
});
84-
expect(events['mousemove']).toBeDefined();
85-
events['mousedown'](new MouseEvent('mousedown'));
86-
events['mousemove'](
87-
new MouseEvent('mousemove', {
88-
clientX: 40,
89-
clientY: 10,
90-
}),
91-
);
132+
expect(events.pointermove).toBeDefined();
133+
134+
// 按下指针
135+
const pointerDown = createPointerEvent('pointerdown', 1, 0, 0);
136+
events.pointerdown(pointerDown);
137+
138+
// 移动超过阈值
139+
const pointerMove = createPointerEvent('pointermove', 1, 40, 10);
140+
events.pointermove(pointerMove);
141+
92142
jest.advanceTimersByTime(320);
93143
expect(mockCallback).not.toBeCalled();
94144

95145
unmount();
96-
expect(events['mousemove']).toBeUndefined();
146+
expect(events.pointermove).toBeUndefined();
147+
});
148+
149+
it('should handle multiple pointer interactions correctly', () => {
150+
setup(mockCallback, mockTarget);
151+
152+
// 第一个指针按下
153+
const pointer1Down = createPointerEvent('pointerdown', 1);
154+
events.pointerdown(pointer1Down);
155+
156+
// 第二个指针按下(应该被忽略)
157+
const pointer2Down = createPointerEvent('pointerdown', 2);
158+
events.pointerdown(pointer2Down);
159+
160+
jest.advanceTimersByTime(350);
161+
162+
// 第二个指针抬起(应该被忽略)
163+
const pointer2Up = createPointerEvent('pointerup', 2);
164+
events.pointerup(pointer2Up);
165+
166+
// 第一个指针抬起(应该触发结束事件)
167+
const pointer1Up = createPointerEvent('pointerup', 1);
168+
events.pointerup(pointer1Up);
169+
170+
expect(mockCallback).toBeCalledTimes(1);
171+
expect(mockTarget.setPointerCapture).toBeCalledWith(1);
172+
expect(mockTarget.setPointerCapture).toBeCalledTimes(1);
97173
});
98174

99175
it(`should not work when target don't support addEventListener method`, () => {
@@ -103,7 +179,7 @@ describe('useLongPress', () => {
103179
},
104180
});
105181

106-
setup(() => {}, mockTarget);
182+
setup(() => { }, mockTarget);
107183
expect(Object.keys(events)).toHaveLength(0);
108184
});
109185
});

packages/hooks/src/useLongPress/index.ts

Lines changed: 38 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@ import { useRef } from 'react';
22
import useLatest from '../useLatest';
33
import type { BasicTarget } from '../utils/domTarget';
44
import { getTargetElement } from '../utils/domTarget';
5-
import isBrowser from '../utils/isBrowser';
65
import useEffectWithTarget from '../utils/useEffectWithTarget';
76

8-
type EventType = MouseEvent | TouchEvent;
7+
type EventType = PointerEvent;
98
export interface Options {
109
delay?: number;
1110
moveThreshold?: { x?: number; y?: number };
@@ -25,8 +24,8 @@ function useLongPress(
2524
const timerRef = useRef<ReturnType<typeof setTimeout>>();
2625
const isTriggeredRef = useRef(false);
2726
const pervPositionRef = useRef({ x: 0, y: 0 });
28-
const mousePressed = useRef(false);
29-
const touchPressed = useRef(false);
27+
const isPressed = useRef(false);
28+
const pointerId = useRef<number | null>(null);
3029
const hasMoveThreshold = !!(
3130
(moveThreshold?.x && moveThreshold.x > 0) ||
3231
(moveThreshold?.y && moveThreshold.y > 0)
@@ -40,94 +39,56 @@ function useLongPress(
4039
}
4140

4241
const overThreshold = (event: EventType) => {
43-
const { clientX, clientY } = getClientPosition(event);
44-
const offsetX = Math.abs(clientX - pervPositionRef.current.x);
45-
const offsetY = Math.abs(clientY - pervPositionRef.current.y);
42+
const offsetX = Math.abs(event.clientX - pervPositionRef.current.x);
43+
const offsetY = Math.abs(event.clientY - pervPositionRef.current.y);
4644

4745
return !!(
4846
(moveThreshold?.x && offsetX > moveThreshold.x) ||
4947
(moveThreshold?.y && offsetY > moveThreshold.y)
5048
);
5149
};
5250

53-
function getClientPosition(event: EventType) {
54-
if ('TouchEvent' in window && event instanceof TouchEvent) {
55-
return {
56-
clientX: event.touches[0].clientX,
57-
clientY: event.touches[0].clientY,
58-
};
59-
}
60-
if (event instanceof MouseEvent) {
61-
return {
62-
clientX: event.clientX,
63-
clientY: event.clientY,
64-
};
65-
}
66-
67-
console.warn('Unsupported event type');
68-
69-
return { clientX: 0, clientY: 0 };
70-
}
71-
7251
const createTimer = (event: EventType) => {
7352
timerRef.current = setTimeout(() => {
7453
onLongPressRef.current(event);
7554
isTriggeredRef.current = true;
7655
}, delay);
7756
};
7857

79-
const onTouchStart = (event: TouchEvent) => {
80-
if (touchPressed.current) return;
81-
touchPressed.current = true;
58+
const onPointerDown = (event: PointerEvent) => {
59+
// 只处理第一个按下的指针
60+
if (isPressed.current) return;
8261

83-
if (hasMoveThreshold) {
84-
const { clientX, clientY } = getClientPosition(event);
85-
pervPositionRef.current.x = clientX;
86-
pervPositionRef.current.y = clientY;
87-
}
88-
createTimer(event);
89-
};
62+
isPressed.current = true;
63+
pointerId.current = event.pointerId;
9064

91-
const onMouseDown = (event: MouseEvent) => {
92-
if ((event as any)?.sourceCapabilities?.firesTouchEvents) return;
93-
94-
mousePressed.current = true;
65+
// 捕获指针以确保即使指针移出元素也能接收到事件
66+
targetElement.setPointerCapture(event.pointerId);
9567

9668
if (hasMoveThreshold) {
9769
pervPositionRef.current.x = event.clientX;
9870
pervPositionRef.current.y = event.clientY;
9971
}
72+
10073
createTimer(event);
10174
};
10275

103-
const onMove = (event: EventType) => {
76+
const onPointerMove = (event: PointerEvent) => {
77+
// 只处理已按下的指针
78+
if (!isPressed.current || event.pointerId !== pointerId.current) return;
79+
10480
if (timerRef.current && overThreshold(event)) {
10581
clearTimeout(timerRef.current);
10682
timerRef.current = undefined;
10783
}
10884
};
10985

110-
const onTouchEnd = (event: TouchEvent) => {
111-
if (!touchPressed.current) return;
112-
touchPressed.current = false;
86+
const onPointerUp = (event: PointerEvent) => {
87+
// 只处理已按下的指针
88+
if (!isPressed.current || event.pointerId !== pointerId.current) return;
11389

114-
if (timerRef.current) {
115-
clearTimeout(timerRef.current);
116-
timerRef.current = undefined;
117-
}
118-
119-
if (isTriggeredRef.current) {
120-
onLongPressEndRef.current?.(event);
121-
} else if (onClickRef.current) {
122-
onClickRef.current(event);
123-
}
124-
isTriggeredRef.current = false;
125-
};
126-
127-
const onMouseUp = (event: MouseEvent) => {
128-
if ((event as any)?.sourceCapabilities?.firesTouchEvents) return;
129-
if (!mousePressed.current) return;
130-
mousePressed.current = false;
90+
isPressed.current = false;
91+
pointerId.current = null;
13192

13293
if (timerRef.current) {
13394
clearTimeout(timerRef.current);
@@ -139,32 +100,34 @@ function useLongPress(
139100
} else if (onClickRef.current) {
140101
onClickRef.current(event);
141102
}
103+
142104
isTriggeredRef.current = false;
143105
};
144106

145-
const onMouseLeave = (event: MouseEvent) => {
146-
if (!mousePressed.current) return;
147-
mousePressed.current = false;
107+
const onPointerCancel = (event: PointerEvent) => {
108+
// 只处理已按下的指针
109+
if (!isPressed.current || event.pointerId !== pointerId.current) return;
110+
111+
isPressed.current = false;
112+
pointerId.current = null;
148113

149114
if (timerRef.current) {
150115
clearTimeout(timerRef.current);
151116
timerRef.current = undefined;
152117
}
118+
153119
if (isTriggeredRef.current) {
154120
onLongPressEndRef.current?.(event);
155121
isTriggeredRef.current = false;
156122
}
157123
};
158124

159-
targetElement.addEventListener('mousedown', onMouseDown);
160-
targetElement.addEventListener('mouseup', onMouseUp);
161-
targetElement.addEventListener('mouseleave', onMouseLeave);
162-
targetElement.addEventListener('touchstart', onTouchStart);
163-
targetElement.addEventListener('touchend', onTouchEnd);
125+
targetElement.addEventListener('pointerdown', onPointerDown);
126+
targetElement.addEventListener('pointerup', onPointerUp);
127+
targetElement.addEventListener('pointercancel', onPointerCancel);
164128

165129
if (hasMoveThreshold) {
166-
targetElement.addEventListener('mousemove', onMove);
167-
targetElement.addEventListener('touchmove', onMove);
130+
targetElement.addEventListener('pointermove', onPointerMove);
168131
}
169132

170133
return () => {
@@ -173,15 +136,12 @@ function useLongPress(
173136
isTriggeredRef.current = false;
174137
}
175138

176-
targetElement.removeEventListener('mousedown', onMouseDown);
177-
targetElement.removeEventListener('mouseup', onMouseUp);
178-
targetElement.removeEventListener('mouseleave', onMouseLeave);
179-
targetElement.removeEventListener('touchstart', onTouchStart);
180-
targetElement.removeEventListener('touchend', onTouchEnd);
139+
targetElement.removeEventListener('pointerdown', onPointerDown);
140+
targetElement.removeEventListener('pointerup', onPointerUp);
141+
targetElement.removeEventListener('pointercancel', onPointerCancel);
181142

182143
if (hasMoveThreshold) {
183-
targetElement.removeEventListener('mousemove', onMove);
184-
targetElement.removeEventListener('touchmove', onMove);
144+
targetElement.removeEventListener('pointermove', onPointerMove);
185145
}
186146
};
187147
},

0 commit comments

Comments
 (0)