From fbc317517d68d13c522c7476ba4c213f7394de09 Mon Sep 17 00:00:00 2001 From: Suhrid Marwah Date: Mon, 2 Mar 2026 05:36:42 +0000 Subject: [PATCH] fix: populate touches[] from changedTouches during touchend During a touchend event the W3C Touch Events spec moves the lifted finger out of e.touches (active contacts only) and into e.changedTouches. _updateTouchCoords iterated over e.touches.length, which is 0 when the last finger lifts, so touches[] was always empty inside touchEnded(). Fall back to e.changedTouches when e.touches is empty so that the final touch position is accessible to user code in touchEnded(). getTouchInfo already contained the e.touches[i] || e.changedTouches[i] fallback for individual lookups; this change fixes the loop bound that prevented it from being reached. Fixes: touches[] always being [] inside touchEnded() on single-touch --- src/events/touch.js | 6 +++++- test/unit/events/touch.js | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/events/touch.js b/src/events/touch.js index ff2587f1bc..7ada0a12e6 100644 --- a/src/events/touch.js +++ b/src/events/touch.js @@ -95,7 +95,11 @@ p5.prototype.touches = []; p5.prototype._updateTouchCoords = function(e) { if (this._curElement !== null) { const touches = []; - for (let i = 0; i < e.touches.length; i++) { + // During a touchend event, the lifted finger is removed from e.touches + // (W3C spec) and is only present in e.changedTouches. Fall back to + // e.changedTouches so that touches[] is populated inside touchEnded(). + const touchList = e.touches.length > 0 ? e.touches : e.changedTouches; + for (let i = 0; i < touchList.length; i++) { touches[i] = getTouchInfo( this._curElement.elt, this.width, diff --git a/test/unit/events/touch.js b/test/unit/events/touch.js index 776b1095c6..2f0e27dd5b 100644 --- a/test/unit/events/touch.js +++ b/test/unit/events/touch.js @@ -138,6 +138,24 @@ suite('Touch Events', function() { }); suite('touchEnded', function() { + test('touches[] should contain changedTouches data inside touchEnded', function() { + // During touchend, the W3C spec removes the lifted finger from e.touches + // and places it in e.changedTouches only. Verify that p5 surfaces those + // positions in the touches[] array so user code can read them. + let touchesInsideCallback = null; + myp5.touchEnded = function() { + touchesInsideCallback = [...myp5.touches]; + }; + const endEvent = new TouchEvent('touchend', { + touches: [], // spec: lifted touch is gone from touches + changedTouches: [touchObj1] // spec: lifted touch lives here + }); + window.dispatchEvent(endEvent); + assert.isNotNull(touchesInsideCallback); + assert.strictEqual(touchesInsideCallback.length, 1); + assert.strictEqual(touchesInsideCallback[0].id, 36); + }); + test('touchEnded must run when a touch is registered', function() { let count = 0; myp5.touchEnded = function() {