Skip to content

Commit 12a8c67

Browse files
cursoragentregenrek
andcommitted
feat: Implement reliable iOS double-click handling hook
Co-authored-by: kevin <kevin@macherjek.at>
1 parent b60012c commit 12a8c67

6 files changed

Lines changed: 1253 additions & 0 deletions

File tree

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
# iOS Double-Click Handling
2+
3+
## Overview
4+
5+
This document describes the implementation and usage of the iOS double-click fix, which addresses reliability issues with double-click/tap interactions on iOS devices (iOS 18+).
6+
7+
## Problem Statement
8+
9+
iOS devices have historically had issues with double-click event detection due to:
10+
11+
1. **300ms Click Delay**: Older iOS versions introduced a delay to distinguish between single taps and double taps
12+
2. **Touch vs Mouse Events**: iOS fires both touch and mouse events, which can cause conflicts
13+
3. **Ghost Clicks**: After a touch event, iOS may fire a duplicate mouse click event 300ms later
14+
4. **Event Timing**: Inconsistent timing between rapid touches on iOS Safari
15+
16+
## Solution
17+
18+
We've implemented a custom React hook (`useDoubleClick`) that provides:
19+
20+
- Reliable double-click/tap detection across all platforms
21+
- Special optimizations for iOS devices
22+
- Configurable delay timing
23+
- Separate handling for single and double clicks
24+
- Ghost click prevention
25+
26+
## Usage
27+
28+
### Basic Double-Click Detection
29+
30+
```typescript
31+
import { useDoubleClick } from '~/hooks/use-double-click';
32+
33+
function MyComponent() {
34+
const handlers = useDoubleClick({
35+
onDoubleClick: () => {
36+
console.log('Double clicked!');
37+
},
38+
delay: 300, // optional, defaults to 300ms
39+
});
40+
41+
return <button {...handlers}>Double Click Me</button>;
42+
}
43+
```
44+
45+
### Distinguishing Single and Double Clicks
46+
47+
```typescript
48+
import { useDoubleClick } from '~/hooks/use-double-click';
49+
50+
function MyComponent() {
51+
const handlers = useDoubleClick({
52+
onSingleClick: () => {
53+
console.log('Single click - select item');
54+
},
55+
onDoubleClick: () => {
56+
console.log('Double click - open item');
57+
},
58+
delay: 300,
59+
});
60+
61+
return <div {...handlers}>Click or Double-Click Me</div>;
62+
}
63+
```
64+
65+
### Double-Click Only (Simplified)
66+
67+
```typescript
68+
import { useDoubleClickOnly } from '~/hooks/use-double-click';
69+
70+
function MyComponent() {
71+
const handlers = useDoubleClickOnly(() => {
72+
console.log('Double clicked!');
73+
}, 300);
74+
75+
return <div {...handlers}>Double-Click Only</div>;
76+
}
77+
```
78+
79+
## API Reference
80+
81+
### `useDoubleClick(options)`
82+
83+
Main hook for handling double-click events.
84+
85+
#### Parameters
86+
87+
- `options` (object):
88+
- `delay` (number, optional): Time window in milliseconds to detect double-click. Default: 300
89+
- `onSingleClick` (function, optional): Callback for single click events
90+
- `onDoubleClick` (function, optional): Callback for double click events
91+
92+
#### Returns
93+
94+
Object containing event handlers to spread onto target element:
95+
- `onClick`: Mouse click handler
96+
- `onTouchEnd`: Touch event handler (iOS only)
97+
- `onContextMenu`: Context menu handler (iOS only, prevents long-press menu)
98+
99+
### `useDoubleClickOnly(callback, delay)`
100+
101+
Simplified hook that only handles double-clicks.
102+
103+
#### Parameters
104+
105+
- `callback` (function): Function to call on double-click
106+
- `delay` (number, optional): Time window in milliseconds. Default: 300
107+
108+
#### Returns
109+
110+
Event handlers object (same as `useDoubleClick`)
111+
112+
## Implementation Details
113+
114+
### iOS Detection
115+
116+
The hook automatically detects iOS devices using:
117+
118+
```typescript
119+
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
120+
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
121+
```
122+
123+
### Ghost Click Prevention
124+
125+
On iOS, after a touch event, the hook prevents duplicate mouse clicks within 500ms:
126+
127+
```typescript
128+
if (isIOS && Date.now() - lastTouchEnd.current < 500) {
129+
event.preventDefault();
130+
return;
131+
}
132+
```
133+
134+
### Touch Event Handling
135+
136+
For iOS devices, the hook provides `onTouchEnd` handlers that:
137+
1. Prevent rapid duplicate touches (< 50ms apart)
138+
2. Track touch timing separately from mouse clicks
139+
3. Properly handle the click sequence
140+
141+
### Timer Management
142+
143+
The hook uses `setTimeout` to distinguish between single and double clicks:
144+
- On first click, starts a timer
145+
- If second click occurs within delay, triggers double-click
146+
- If timer expires, triggers single-click
147+
148+
All timers are cleaned up on component unmount.
149+
150+
## Testing
151+
152+
The implementation includes comprehensive tests covering:
153+
154+
- Basic double-click detection
155+
- Single click detection
156+
- Custom delay timing
157+
- iOS-specific behavior
158+
- Ghost click prevention
159+
- Touch event handling
160+
- Edge cases (triple-clicks, rapid clicks, etc.)
161+
162+
Run tests with:
163+
164+
```bash
165+
pnpm vitest --run tests/use-double-click.test.ts
166+
```
167+
168+
## Examples
169+
170+
See the demo component at `src/components/examples/DoubleClickDemo.tsx` for interactive examples.
171+
172+
## Browser Compatibility
173+
174+
- **iOS Safari**: iOS 12+
175+
- **Chrome (iOS)**: iOS 12+
176+
- **Firefox (iOS)**: iOS 12+
177+
- **Desktop Browsers**: All modern browsers
178+
- **Android**: All modern browsers
179+
180+
## Performance Considerations
181+
182+
- Minimal overhead: Uses refs and callbacks to avoid unnecessary re-renders
183+
- No external dependencies
184+
- Automatic cleanup of timers
185+
- Efficient event handler memoization
186+
187+
## Troubleshooting
188+
189+
### Double-clicks not registering on iOS
190+
191+
1. Verify the delay is appropriate (300ms is recommended)
192+
2. Check that the element isn't being re-rendered between clicks
193+
3. Ensure no other event handlers are preventing propagation
194+
4. Test with actual device (simulator behavior may differ)
195+
196+
### Single clicks firing when they shouldn't
197+
198+
1. Increase the delay value
199+
2. Use `useDoubleClickOnly` if you don't need single-click detection
200+
3. Check for conflicting onClick handlers
201+
202+
### Ghost clicks occurring
203+
204+
The hook should prevent this automatically on iOS. If issues persist:
205+
1. Verify the hook's event handlers are properly spread onto the element
206+
2. Ensure no parent elements have conflicting touch handlers
207+
3. Check that `preventDefault` isn't being called elsewhere
208+
209+
## References
210+
211+
- [iOS Touch Event Handling](https://developer.apple.com/documentation/webkitjs/handling_events/touchevents)
212+
- [MDN: Touch Events](https://developer.mozilla.org/en-US/docs/Web/API/Touch_events)
213+
- [300ms Click Delay](https://developers.google.com/web/updates/2013/12/300ms-tap-delay-gone-away)
214+
215+
## Contributing
216+
217+
When modifying the double-click implementation:
218+
219+
1. Run the test suite to ensure no regressions
220+
2. Test on actual iOS devices (multiple versions if possible)
221+
3. Update this documentation with any behavior changes
222+
4. Add tests for new features or bug fixes

0 commit comments

Comments
 (0)