Switch canvas pointer events to native addEventListener

React's synthetic event system delegates to the document root, which
on iOS Safari causes pointermove passive:true to block preventDefault
and introduces timing issues that drop every other stroke.

Attaching directly to the canvas element with {passive: false} on
pointermove gives iOS the explicit signal it needs to suppress scroll
and deliver all pen events reliably.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmert
2026-08-12 09:23:16 -04:00
parent cf040ff8dd
commit 717a9b3da0
+60 -45
View File
@@ -68,51 +68,70 @@ export default function DrawCanvas({ strokes, onStrokesChange, onDone, headerCon
]; ];
}, []); }, []);
const onPointerDown = useCallback((e) => { // Use native addEventListener (not React synthetic events) so that
if (e.pointerType === 'touch') return; // passive:false works correctly on iOS Safari and events are handled
isDrawingRef.current = true; // directly on the canvas element rather than via document-root delegation.
activeStrokeRef.current = [getPoint(e)]; useEffect(() => {
render(); const canvas = canvasRef.current;
}, [getPoint, render]); if (!canvas) return;
const onPointerMove = useCallback((e) => { function down(e) {
if (e.pointerType === 'touch') return; if (e.pointerType === 'touch') return;
if (!isDrawingRef.current) return; isDrawingRef.current = true;
e.preventDefault(); activeStrokeRef.current = [getPoint(e)];
const pt = getPoint(e); render();
activeStrokeRef.current = [...activeStrokeRef.current, pt];
if (drawToolRef.current === 'eraser') {
const [ex, ey] = pt;
const r = drawSizeRef.current * 3;
const remaining = strokesRef.current.filter(
(s) => !s.points.some(([sx, sy]) => Math.hypot(sx - ex, sy - ey) < r),
);
if (remaining.length !== strokesRef.current.length)
onStrokesChangeRef.current(remaining);
} }
render();
}, [getPoint, render]);
const onPointerUp = useCallback((e) => { function move(e) {
if (e.pointerType === 'touch') return; if (e.pointerType === 'touch') return;
if (!isDrawingRef.current) return; if (!isDrawingRef.current) return;
isDrawingRef.current = false; e.preventDefault();
const pts = activeStrokeRef.current; const pt = getPoint(e);
if (drawToolRef.current !== 'eraser' && pts.length > 1) { activeStrokeRef.current = [...activeStrokeRef.current, pt];
onStrokesChangeRef.current([ if (drawToolRef.current === 'eraser') {
...strokesRef.current, const [ex, ey] = pt;
{ const r = drawSizeRef.current * 3;
id: crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`, const remaining = strokesRef.current.filter(
tool: drawToolRef.current, (s) => !s.points.some(([sx, sy]) => Math.hypot(sx - ex, sy - ey) < r),
color: drawColorRef.current, );
size: drawSizeRef.current, if (remaining.length !== strokesRef.current.length)
points: pts, onStrokesChangeRef.current(remaining);
}, }
]); render();
} }
activeStrokeRef.current = [];
render(); function up(e) {
}, [render]); if (e.pointerType === 'touch') return;
if (!isDrawingRef.current) return;
isDrawingRef.current = false;
const pts = activeStrokeRef.current;
if (drawToolRef.current !== 'eraser' && pts.length > 1) {
onStrokesChangeRef.current([
...strokesRef.current,
{
id: crypto.randomUUID?.() ?? `${Date.now()}-${Math.random()}`,
tool: drawToolRef.current,
color: drawColorRef.current,
size: drawSizeRef.current,
points: pts,
},
]);
}
activeStrokeRef.current = [];
render();
}
canvas.addEventListener('pointerdown', down);
canvas.addEventListener('pointermove', move, { passive: false });
canvas.addEventListener('pointerup', up);
canvas.addEventListener('pointercancel', up);
return () => {
canvas.removeEventListener('pointerdown', down);
canvas.removeEventListener('pointermove', move);
canvas.removeEventListener('pointerup', up);
canvas.removeEventListener('pointercancel', up);
};
}, [getPoint, render]);
const undo = () => strokes.length > 0 && onStrokesChange(strokes.slice(0, -1)); const undo = () => strokes.length > 0 && onStrokesChange(strokes.slice(0, -1));
const clear = () => const clear = () =>
@@ -236,10 +255,6 @@ export default function DrawCanvas({ strokes, onStrokesChange, onDone, headerCon
cursor: drawTool === 'eraser' ? 'cell' : 'crosshair', cursor: drawTool === 'eraser' ? 'cell' : 'crosshair',
touchAction: 'none', touchAction: 'none',
}} }}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onPointerCancel={onPointerUp}
/> />
</div> </div>
</div> </div>