Skip to content

Commit b9ad0e6

Browse files
authored
[fix] Prevent click events during touch start (#82)
* fix #47 * memoize event listener functions, fix cleanup
1 parent eb93f57 commit b9ad0e6

File tree

1 file changed

+195
-147
lines changed

1 file changed

+195
-147
lines changed

src/hooks/useClockEvents.ts

Lines changed: 195 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -27,177 +27,225 @@ export default function useClockEvents(
2727
const wrapper = useRef<HTMLDivElement | null>(null)
2828
const calcOffsetCache: React.MutableRefObject<null | CalcOffsetFn> = useRef(null)
2929
const dragCount = useRef(0)
30-
const cleanup = useCallback(_removeHandlers, [])
31-
const disableMouse = useRef(false)
30+
const cleanupRef = useRef<() => void>(() => {})
3231

33-
// mouse events
34-
function handleMouseDown(e: React.MouseEvent<HTMLElement>) {
35-
if (disableMouse.current) {
36-
return
37-
}
38-
dragCount.current = 0
32+
// avoid recomputing all the event listeners, prolly unnecessary...
33+
const handleChangeRef = useRef(handleChange)
34+
useEffect(() => {
35+
handleChangeRef.current = handleChange
36+
}, [handleChange])
37+
38+
const calculatePoint = useCallback(
39+
(
40+
offsetX: number,
41+
offsetY: number,
42+
// determines if change is due to mouseup/touchend in order to
43+
// automatically change unit (eg: hour -> minute) if enabled
44+
// prevents changing unit if dragging along clock
45+
canAutoChangeMode: boolean,
46+
) => {
47+
// if user just clicks/taps a number (drag count < 2), then just assume it's a rough tap
48+
// and force a rounded/coarse number (ie: 1, 2, 3, 4 is tapped, assume 0 or 5)
49+
const wasTapped = dragCount.current < 2
3950

40-
// terminate if click is outside of clock radius, ie:
41-
// if clicking meridiem button which overlaps with clock
42-
if (clock.current) {
43-
calcOffsetCache.current = calcOffset(clock.current)
44-
const { offsetX, offsetY } = calcOffsetCache.current!(e.clientX, e.clientY)
4551
const x = offsetX - CLOCK_RADIUS
46-
const y = offsetY - CLOCK_RADIUS
47-
if (!isWithinRadius(x, y, CLOCK_RADIUS)) return
48-
}
52+
const y = -offsetY + CLOCK_RADIUS
53+
54+
const a = atan2(y, x)
55+
let d = 90 - deg(a)
56+
if (d < 0) {
57+
d = 360 + d
58+
}
59+
60+
// ensure touch doesn't bleed outside of clock radius
61+
if (!isWithinRadius(x, y, CLOCK_RADIUS) && wasTapped) {
62+
return false
63+
}
64+
const isInnerClick = isWithinRadius(x, y, INNER_NUMBER_RADIUS)
65+
66+
// update time on main
67+
handleChangeRef.current(d, { canAutoChangeMode, wasTapped, isInnerClick })
68+
},
69+
[],
70+
)
4971

50-
// add listeners
51-
document.addEventListener('mousemove', handleMouseDrag, false)
52-
document.addEventListener('mouseup', handleStopDrag, false)
53-
wrapper.current &&
54-
wrapper.current.addEventListener('mouseleave', handleStopDrag, false)
72+
// handle mouse + touch changes
73+
const handleMouseUp = useCallback(
74+
(e: MouseEvent) => {
75+
if (!clock.current) {
76+
return
77+
}
78+
clock.current.style.cursor = ''
5579

56-
// @ts-ignore
57-
handleMouseDrag(e)
58-
}
59-
function handleMouseDrag(e: MouseEvent) {
60-
if (calcOffsetCache.current) {
61-
const { offsetX, offsetY } = calcOffsetCache.current(e.clientX, e.clientY)
62-
calculatePoint(offsetX, offsetY, false)
63-
}
64-
dragCount.current++
80+
const { offsetX, offsetY } = calcOffsetCache.current!(e.clientX, e.clientY)
81+
calculatePoint(offsetX, offsetY, true)
82+
},
83+
[calculatePoint, clock],
84+
)
85+
const handleTouchEnd = useCallback(
86+
(e: TouchEvent) => {
87+
const touch = e.targetTouches[0] || e.changedTouches[0]
88+
if (touch && calcOffsetCache.current) {
89+
const { offsetX, offsetY } = calcOffsetCache.current(
90+
touch.clientX,
91+
touch.clientY,
92+
)
93+
calculatePoint(offsetX, offsetY, true)
94+
}
95+
},
96+
[calculatePoint],
97+
)
98+
99+
const handleMouseDrag = useCallback(
100+
(e: MouseEvent) => {
101+
if (calcOffsetCache.current) {
102+
const { offsetX, offsetY } = calcOffsetCache.current(e.clientX, e.clientY)
103+
calculatePoint(offsetX, offsetY, false)
104+
}
105+
dragCount.current++
106+
107+
if (dragCount.current === 1 && clock.current) {
108+
clock.current.style.cursor = '-webkit-grabbing'
109+
clock.current.style.cursor = 'grabbing'
110+
}
111+
112+
e.preventDefault()
113+
return false
114+
},
115+
[calculatePoint, clock],
116+
)
117+
const handleTouchDrag = useCallback(
118+
(e: TouchEvent) => {
119+
if (calcOffsetCache.current) {
120+
const touch = e.targetTouches[0]
121+
const { offsetX, offsetY } = calcOffsetCache.current(
122+
touch.clientX,
123+
touch.clientY,
124+
)
125+
calculatePoint(offsetX, offsetY, false)
126+
}
127+
dragCount.current++
128+
129+
e.preventDefault()
130+
return false
131+
},
132+
[calculatePoint],
133+
)
65134

66-
if (dragCount.current === 1 && clock.current) {
67-
clock.current.style.cursor = '-webkit-grabbing'
68-
clock.current.style.cursor = 'grabbing'
69-
}
135+
// stop mouse + touch events
136+
const handleStopDrag = useCallback(
137+
(e: MouseEvent | TouchEvent) => {
138+
cleanupRef.current()
139+
140+
if (e == null || clock.current == null) {
141+
return
142+
}
143+
144+
if (isMouseEventEnd(e)) {
145+
handleMouseUp(e)
146+
} else if (isTouchEventEnd(e)) {
147+
handleTouchEnd(e)
148+
}
149+
150+
function isMouseEventEnd(e: MouseEvent | TouchEvent): e is MouseEvent {
151+
return e.type === 'mouseup'
152+
}
153+
function isTouchEventEnd(e: MouseEvent | TouchEvent): e is TouchEvent {
154+
return e.type === 'touchcancel' || e.type === 'touchend'
155+
}
156+
},
157+
[handleMouseUp, handleTouchEnd, clock],
158+
)
70159

71-
e.preventDefault()
72-
return false
73-
}
160+
// mouse events
161+
const handleMouseDown = useCallback(
162+
(e: React.MouseEvent<HTMLElement>) => {
163+
dragCount.current = 0
164+
165+
// terminate if click is outside of clock radius, ie:
166+
// if clicking meridiem button which overlaps with clock
167+
if (clock.current) {
168+
calcOffsetCache.current = calcOffset(clock.current)
169+
const { offsetX, offsetY } = calcOffsetCache.current!(
170+
e.clientX,
171+
e.clientY,
172+
)
173+
const x = offsetX - CLOCK_RADIUS
174+
const y = offsetY - CLOCK_RADIUS
175+
if (!isWithinRadius(x, y, CLOCK_RADIUS)) return
176+
}
177+
178+
// add listeners
179+
document.addEventListener('mousemove', handleMouseDrag, false)
180+
document.addEventListener('mouseup', handleStopDrag, false)
181+
wrapper.current &&
182+
wrapper.current.addEventListener('mouseleave', handleStopDrag, false)
183+
184+
// @ts-ignore
185+
handleMouseDrag(e)
186+
},
187+
[clock, handleMouseDrag, handleStopDrag],
188+
)
74189

75190
// touch events
76-
function handleTouchStart() {
77-
// disables mouse events during touch events
78-
disableMouse.current = true
79-
dragCount.current = 0
80-
81-
// add listeners
82-
document.addEventListener('touchmove', touchDragHandler, false)
83-
document.addEventListener('touchend', handleStopDrag, false)
84-
document.addEventListener('touchcancel', handleStopDrag, false)
85-
86-
if (clock.current) {
87-
calcOffsetCache.current = calcOffset(clock.current)
88-
}
89-
}
90-
function touchDragHandler(e: TouchEvent) {
91-
if (calcOffsetCache.current) {
92-
const touch = e.targetTouches[0]
93-
const { offsetX, offsetY } = calcOffsetCache.current(
94-
touch.clientX,
95-
touch.clientY,
96-
)
97-
calculatePoint(offsetX, offsetY, false)
98-
}
99-
dragCount.current++
100-
101-
e.preventDefault()
102-
return false
103-
}
104-
105-
// stop mouse + touch events
106-
function handleStopDrag(e: MouseEvent | TouchEvent) {
107-
_removeHandlers()
108-
109-
if (e == null || clock.current == null) {
110-
return
111-
}
191+
const handleTouchStart = useCallback(
192+
(e: TouchEvent) => {
193+
e.preventDefault()
112194

113-
if (isMouseEventEnd(e)) {
114-
_handleMouseUp(e)
115-
} else if (isTouchEventEnd(e)) {
116-
_handleTouchEnd(e)
117-
}
195+
dragCount.current = 0
118196

119-
function isMouseEventEnd(e: MouseEvent | TouchEvent): e is MouseEvent {
120-
return e.type === 'mouseup'
121-
}
122-
function isTouchEventEnd(e: MouseEvent | TouchEvent): e is TouchEvent {
123-
return e.type === 'touchcancel' || e.type === 'touchend'
124-
}
125-
}
126-
function _removeHandlers() {
127-
document.removeEventListener('mousemove', handleMouseDrag, false)
128-
document.removeEventListener('mouseup', handleStopDrag, false)
129-
wrapper.current &&
130-
wrapper.current.removeEventListener('mouseleave', handleStopDrag, false)
131-
132-
document.removeEventListener('touchmove', touchDragHandler, false)
133-
document.removeEventListener('touchend', handleStopDrag, false)
134-
document.removeEventListener('touchcancel', handleStopDrag, false)
135-
}
197+
// add listeners
198+
document.addEventListener('touchmove', handleTouchDrag, false)
199+
document.addEventListener('touchend', handleStopDrag, false)
200+
document.addEventListener('touchcancel', handleStopDrag, false)
136201

137-
// handle mouse + touch changes
138-
function _handleMouseUp(e: MouseEvent) {
139-
if (!clock.current) {
140-
return
141-
}
142-
clock.current.style.cursor = ''
202+
if (clock.current) {
203+
calcOffsetCache.current = calcOffset(clock.current)
204+
}
205+
},
206+
[clock, handleStopDrag, handleTouchDrag],
207+
)
143208

144-
const { offsetX, offsetY } = calcOffsetCache.current!(e.clientX, e.clientY)
145-
calculatePoint(offsetX, offsetY, true)
146-
}
147-
function _handleTouchEnd(e: TouchEvent) {
148-
const touch = e.targetTouches[0] || e.changedTouches[0]
149-
if (touch && calcOffsetCache.current) {
150-
const { offsetX, offsetY } = calcOffsetCache.current(
151-
touch.clientX,
152-
touch.clientY,
153-
)
154-
calculatePoint(offsetX, offsetY, true)
209+
// attach touchstart event manually to the clock to make it cancelable.
210+
useEffect(() => {
211+
const currentTarget = clock.current
212+
const type = 'touchstart'
213+
if (currentTarget) {
214+
currentTarget.addEventListener(type, handleTouchStart, false)
155215
}
156-
157-
setTimeout(() => {
158-
disableMouse.current = false
159-
}, 10)
160-
}
161-
function calculatePoint(
162-
offsetX: number,
163-
offsetY: number,
164-
// determines if change is due to mouseup/touchend in order to
165-
// automatically change unit (eg: hour -> minute) if enabled
166-
// prevents changing unit if dragging along clock
167-
canAutoChangeMode: boolean,
168-
) {
169-
// if user just clicks/taps a number (drag count < 2), then just assume it's a rough tap
170-
// and force a rounded/coarse number (ie: 1, 2, 3, 4 is tapped, assume 0 or 5)
171-
const wasTapped = dragCount.current < 2
172-
173-
const x = offsetX - CLOCK_RADIUS
174-
const y = -offsetY + CLOCK_RADIUS
175-
176-
const a = atan2(y, x)
177-
let d = 90 - deg(a)
178-
if (d < 0) {
179-
d = 360 + d
216+
return () => {
217+
if (currentTarget) {
218+
currentTarget.removeEventListener(type, handleTouchStart, false)
219+
}
180220
}
221+
}, [clock, handleTouchStart])
181222

182-
// ensure touch doesn't bleed outside of clock radius
183-
if (!isWithinRadius(x, y, CLOCK_RADIUS) && wasTapped) {
184-
return false
223+
/*
224+
deals with circular dependencies of callback functions; add listener function
225+
depends on remove listener function and vice versa
226+
on remove listener function which depends on the add listener cb
227+
*/
228+
useEffect(() => {
229+
cleanupRef.current = () => {
230+
document.removeEventListener('mousemove', handleMouseDrag, false)
231+
document.removeEventListener('mouseup', handleStopDrag, false)
232+
wrapper.current &&
233+
wrapper.current.removeEventListener('mouseleave', handleStopDrag, false)
234+
235+
document.removeEventListener('touchmove', handleTouchDrag, false)
236+
document.removeEventListener('touchend', handleStopDrag, false)
237+
document.removeEventListener('touchcancel', handleStopDrag, false)
185238
}
186-
const isInnerClick = isWithinRadius(x, y, INNER_NUMBER_RADIUS)
187-
188-
// update time on main
189-
handleChange(d, { canAutoChangeMode, wasTapped, isInnerClick })
190-
}
239+
}, [handleMouseDrag, handleStopDrag, handleTouchDrag])
191240

192241
// clean up
193242
useEffect(() => {
194-
return cleanup
195-
}, [cleanup])
243+
return cleanupRef.current
244+
}, [])
196245

197246
return {
198247
bind: {
199248
onMouseDown: handleMouseDown,
200-
onTouchStart: handleTouchStart,
201249
ref: wrapper,
202250
},
203251
}

0 commit comments

Comments
 (0)