From 3b553914c049a8e4d209d41b8f971b0400cdae0e Mon Sep 17 00:00:00 2001 From: hcooch2ch3 Date: Fri, 5 Jun 2026 12:57:38 +0900 Subject: [PATCH] fix: re-establish touch identifier when a gesture restarts after a missed pointerup When a pointerdown arrives for a pointer that is already down (a missed pointerup), WorkspaceSvg.getGesture cancels the stale gesture and starts a new one. cancel() -> dispose() clears the global touch identifier, but the replacement gesture never re-establishes it, so the terminating pointerup is rejected by Touch.shouldHandleEvent inside Gesture.handleUp (it returns before dispose()). The gesture is never disposed and the workspace locks permanently (Gesture.inProgress() stays true) until reload. Regression since v12.0.0. Re-establish the touch identifier in Gesture.doStart so the gesture's own terminating pointerup is honored and the gesture disposes normally. Adds a regression test. --- packages/blockly/core/gesture.ts | 8 +++++++ packages/blockly/tests/mocha/gesture_test.js | 22 ++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/packages/blockly/core/gesture.ts b/packages/blockly/core/gesture.ts index 9d617c4c62b..aefbce51cb1 100644 --- a/packages/blockly/core/gesture.ts +++ b/packages/blockly/core/gesture.ts @@ -418,6 +418,14 @@ export class Gesture { this.mouseDownXY = new Coordinate(e.clientX, e.clientY); + // Re-establish the touch identifier for this gesture's starting pointer. + // If getGesture() cancelled a stale gesture for this pointer (a missed + // pointerup), that cancel() cleared the global touch identifier via + // dispose(). Without restoring it here, this gesture's terminating + // pointerup is rejected by Touch.shouldHandleEvent and the gesture is + // never disposed, permanently locking the workspace. + Touch.checkTouchIdentifier(e); + this.bindMouseEvents(e); if (!this.isEnding_) { diff --git a/packages/blockly/tests/mocha/gesture_test.js b/packages/blockly/tests/mocha/gesture_test.js index 9036141ef25..5dd90f6b478 100644 --- a/packages/blockly/tests/mocha/gesture_test.js +++ b/packages/blockly/tests/mocha/gesture_test.js @@ -130,4 +130,26 @@ suite('Gesture', function () { this.workspace.id, ); }); + + test('Duplicate pointerdown (missed pointerup) does not lock the workspace', function () { + // Some touch environments emit a pointerdown for a pointer that is already + // down (a missed pointerup). getGesture() handles this by cancelling the + // stale gesture and starting a new one; that new gesture must still be + // disposable so the workspace does not lock up permanently. + const target = this.workspace.getSvgGroup(); + dispatchPointerEvent(target, 'pointerdown', {pointerId: 1}); + assert.isTrue( + Blockly.Gesture.inProgress(), + 'Precondition: first pointerdown must start a gesture.', + ); + // Duplicate pointerdown for the same pointer id (missed pointerup). + dispatchPointerEvent(target, 'pointerdown', {pointerId: 1}); + // Releasing the pointer must end the gesture. + dispatchPointerEvent(document, 'pointerup', {pointerId: 1}); + + assert.isFalse( + Blockly.Gesture.inProgress(), + 'Gesture should not remain in progress after the pointer is released.', + ); + }); });