Summary
A signature‑capture canvas worked flawlessly on desktop but failed to register touch drawing on mobile Safari. The canvas rendered correctly, but touch events never produced usable coordinates, resulting in no visible strokes.
Root Cause
The underlying issue was incorrect handling of touch events on iOS Safari, specifically:
- Using
touchesinstead ofchangedTouchesduringtouchendand sometimestouchmove, which results in missing coordinates. - Event listeners bound to class methods without proper
thiscontext, causingthis.ctxorthis.drawingto be undefined at runtime. - Canvas scaling mismatch between CSS size and actual pixel size, causing touches to map to the wrong coordinates.
- iOS Safari requiring non‑passive listeners for
preventDefault()to work, but the code only partially satisfies this.
Why This Happens in Real Systems
Real production systems frequently hit this problem because:
- iOS Safari has unique touch‑event quirks not present on desktop browsers.
- Developers often assume mouse and touch events behave similarly, but they differ in coordinate models and event lifecycles.
- Angular component methods lose their
thisbinding unless explicitly preserved. - High‑DPI scaling introduces coordinate distortion if not handled consistently.
Real-World Impact
When this bug appears in production:
- Users cannot sign forms, blocking onboarding or checkout flows.
- Support teams receive reports of “the signature pad is broken.”
- Conversion rates drop because mobile users represent the majority of traffic.
- QA teams struggle because the issue does not reproduce on desktop.
Example or Code (if necessary and relevant)
Below is a corrected event‑coordinate handler using changedTouches and ensuring proper binding:
getXY(event: MouseEvent | TouchEvent) {
const rect = this.signatureCanvas.nativeElement.getBoundingClientRect();
if (event instanceof MouseEvent) {
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
}
const touch = event.changedTouches[0];
return {
x: touch.clientX - rect.left,
y: touch.clientY - rect.top
};
}
How Senior Engineers Fix It
Experienced engineers typically resolve this by:
- Normalizing all pointer input using
pointerdown,pointermove,pointerupinstead of mixing mouse and touch events. - Ensuring all event handlers are bound (
this.startDraw = this.startDraw.bind(this)or using arrow functions everywhere). - Using
changedTouchesfor reliable touch coordinates. - Applying consistent scaling between CSS pixels and canvas pixels.
- Adding
{ passive: false }to all touch listeners sopreventDefault()works on iOS.
Why Juniors Miss It
Less experienced developers often overlook this because:
- They assume
touches[0]always exists, but it disappears ontouchend. - They don’t realize that iOS Safari blocks
preventDefault()unless listeners are explicitly non‑passive. - They rely on desktop testing, missing mobile‑specific behavior.
- They are unfamiliar with pointer events, which simplify cross‑device input handling.
- They don’t notice that method references lose their class context when passed directly to event listeners.
This combination makes the bug subtle, frustrating, and easy to miss without deep experience in mobile browser behavior.