Cancelling live interactions - #766
Merged
Merged
Conversation
A directive destroyed as a bystander (e.g. virtualization destroying node or port components during a touch pan) no longer resets the shared TouchEventsStateService.currentEvent, so gesture exclusivity survives. Only the directive whose gesture set the marker clears it — on pointerup, on cancelActiveInteraction, or when destroyed mid-gesture.
Covers every gesture (drag, linking, resize, rotate, pan): state rollback, immediate listener teardown, the cancelled reason on the ended events, the kept viewport after a cancelled pan, the programmatic no-op, and Escape staying non-intrusive when nothing is cancellable.
- move the drag rollback snapshot (initialPositions) off the public DraggingActionState onto the handler-private gesture state, matching the documented rule that gesture bookkeeping stays out of the public action state; regenerate api-report and docs - cancelActiveInteraction: derive the return value from hasActiveInteraction and collect cancel errors in a plain list - panning handlers: no-op cancel() early when no pan is active - pointer-move-selection cancel(): guard clause instead of one big if - tests: reuse macrotask/mockEnvironment from test-utils, collapse the repeated router.emit literals behind an emitGesture helper, share the dragging-state fixture in flow-core tests - e2e fixture: promote nodePosition/centerOf/beginDrag/nextFrame onto the Diagram page object and reuse them in the cancel-interaction spec
- a cancel arriving while the gesture's normal end phase is in flight is refused: the drag handler reads gesture.ended, resize/rotate track the finishing state identity, linking uses a _finishing marker shared by finishLinking and cancelLinking (also prevents duplicate edgeDrawEnded and double-cancel re-entry); handler cancel() now returns whether it tore anything down and cancelActiveInteraction reports that truthfully - cancelActiveInteraction refuses to run while a transaction is active (the rollback would merge into it and could be discarded with it) and ignores concurrent re-entrant calls - gesture directives and ManualLinkingService guard against a second start mid-gesture, which orphaned interaction-cleanup registrations and made hasActiveInteraction() stick to true - minimap navigation joins the cancel machinery: drag registers an interaction cleanup, cancel tears down its listeners and pointer capture, destroy mid-drag clears the panning state it set - tests: cancel-vs-in-flight-end at unit and integration level, transaction guard, concurrent double cancel, group-children rollback, virtualized panning cancel, cancelLinking finishing/identity guards, re-entry and unregister-on-pointerup specs, ManualLinkingService and minimap specs, e2e: selection intact after cancel, Escape free after a completed gesture, programmatic cancel of manual linking
- finishingState in the resize/rotate handlers is set-only: every gesture starts with a fresh state object, so a stale reference can never match a live gesture — the four conditional resets were pure bookkeeping - integration test: hoist the shared startDrag helper instead of a second copy (startDragOn/dragStartEvent removed) - minimap ngOnDestroy uses the existing setPanningState helper - ManualLinkingService.startLinking calls the idempotent cleanup() unconditionally - one canonical re-entry-guard comment across the five gesture directives
- FlowCore keeps one cancellableGestures registry (action-state probe paired with the handler's input-event name) that both hasActiveInteraction() and cancelActiveInteraction() derive from — a new cancellable gesture is one entry, and the dragging/pointerMoveSelection naming seam is an explicit pair instead of a surprise - the teardown-claim concept moves into EventHandler as claimTeardown()/isTeardownClaimed(); resize and rotate drop their twin finishingState fields, and the base-class doc is the single place that explains why drag (gesture.ended) and linking (_finishing) differ - every gesture participant names its teardown removeListeners() (rotate, linking, ManualLinkingService, minimap renamed) - rotate.directive follows the sibling shape: validate targetData before committing any state, tear down before emitting the end phase, pointercancel delegates to pointerup - pointer-move-selection and panning directives clear their core action state when destroyed mid-gesture, like resize/rotate/linking/minimap already did
A gesture started right after Escape could capture geometry from the model before the cancel's rollback transaction committed: the second resize's start phase read the pre-rollback size as its baseline, so the first pointer move snapped the node back to the size from just before Escape. The same window existed for rotate (initial angle), drag (position snapshot) and linking (stale port geometry). FlowCore exposes isCancellingInteraction() and every gesture handler drops incoming input events while it is true — the window closes when the rollback commit resolves, which the awaitable-emit contract guarantees. Reproduced and pinned by an integration test that parks the cancelResize pass on a slow middleware and races a second resize into the window.
The resize and rotate handles stopPropagation() on pointerdown, so the keyboard directive's bubble-phase focus grab never fired for them. A gesture started while focus was outside the diagram (e.g. right after clicking a toolbar button) left every shortcut dead — Escape could not cancel the resize. The focus grab now runs as a capture-phase listener, which fires before any handler can stop propagation. E2e reproduces the exact flow: focus a button outside the diagram, start a resize on the handle, press Escape — the size must roll back.
onPointerDown stays a plain method (the capture listener wraps it), so the api-report only gains the additive OnDestroy members. Adds the missing CHANGELOG entry for the focus fix and regenerates the api docs.
FlowCore keeps the public facade (unchanged signatures and TSDoc, thin delegations); the cleanup registry, the re-entrancy latch, the cancellable gestures registry and the cancel orchestration move into a dedicated collaborator, following the TransactionManager/MeasurementTracker pattern. No caller changes.
Comment-quality pass over the branch's additions: constraints and mechanisms stay, in-progress narration goes. - the four directives' marker guard now states why it exists (the marker keeps concurrent gestures out) and what gestureActive marks (the marker's writer) - EventHandler docs trimmed to contracts; handle() documents the un-awaited invocation interleaving contract the drag handler's comment already pointed at - FlowCore facade docs shortened to match the service canon; the registerInteractionCleanup and isCancellingInteraction docs drop consumer narration - cancel-linking/finish-linking lose the "mirror" narration; the cancelReason write in the drag handler explains it is not a dead write - keyboard-inputs capture comment generalized beyond resize/rotate; minimap gains the canonical re-entry comment and the captureElement rationale
Covers what the FlowCore facade tests cannot see: cleanups run before handler cancels, the cancelling latch is up only while a cancel is in flight and is released on rejection, the transaction guard refuses without touching cleanups or the router, and a re-entrant cancel is a side-effect-free no-op.
lukasz-jazwa
approved these changes
Aug 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.