Skip to content

Merge deploy/jon into main#3355

Merged
SonicScrewdriver merged 2 commits intomainfrom
deploy/jon
Mar 16, 2026
Merged

Merge deploy/jon into main#3355
SonicScrewdriver merged 2 commits intomainfrom
deploy/jon

Conversation

@jonmarkprice
Copy link
Copy Markdown
Contributor

@jonmarkprice jonmarkprice commented Mar 13, 2026

I used git land to land a PR on my deploy branch, but realized I need another one to merge it to main.

jonmarkprice and others added 2 commits March 13, 2026 16:24
…limited graphs (#3340)

## Summary:
On unlimited point and polygon graphs, using the arrow keys to move a point leaves `isCurrentlyDragging` `true`. This makes it so that clicking the graph to add new point no longer works. This happens because we call `setIsCurrentlyDragging(true)` from `onMove` which fires for _both_ mouse drags and keyboard movement. As a result, any keyboard interaction leaves the graph thinking a drag was still in progress. This PR adds a dedicated `onDragStart` callback to `useDraggable` that only fires at the very start of a mouse drag.

Issue: LEMS-3977

## Test plan:
- Test via Storybook by clicking to add a point, then using the keyboard keys to move the point, then clicking elsewhere on the graph again to create a new point.
- Added tests to verify that `onDragStart` fires on mouse drag but not on keyboard movement.

Author: jonmarkprice

Reviewers: claude[bot], SonicScrewdriver, jonmarkprice

Required Reviewers:

Approved By: SonicScrewdriver

Checks: ⏭️  1 check has been skipped, ✅ 10 checks were successful

Pull Request URL: #3340
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 13, 2026

🗄️ Schema Change: No Changes ✅

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 13, 2026

🛠️ Item Splitting: No Changes ✅

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 13, 2026

Size Change: +56 B (+0.01%)

Total Size: 486 kB

Filename Size Change
packages/perseus/dist/es/index.js 187 kB +56 B (+0.03%)
ℹ️ View Unchanged
Filename Size
packages/kas/dist/es/index.js 20.8 kB
packages/keypad-context/dist/es/index.js 1 kB
packages/kmath/dist/es/index.js 5.96 kB
packages/math-input/dist/es/index.js 98.5 kB
packages/math-input/dist/es/strings.js 1.61 kB
packages/perseus-core/dist/es/index.item-splitting.js 11.8 kB
packages/perseus-core/dist/es/index.js 24.9 kB
packages/perseus-editor/dist/es/index.js 100 kB
packages/perseus-linter/dist/es/index.js 8.82 kB
packages/perseus-score/dist/es/index.js 9.26 kB
packages/perseus-utils/dist/es/index.js 403 B
packages/perseus/dist/es/strings.js 7.47 kB
packages/pure-markdown/dist/es/index.js 1.39 kB
packages/simple-markdown/dist/es/index.js 6.71 kB

compressed-size-action

@github-actions
Copy link
Copy Markdown
Contributor

npm Snapshot: Published

Good news!! We've packaged up the latest commit from this PR (a93d628) and published it to npm. You
can install it using the tag PR3355.

Example:

pnpm add @khanacademy/perseus@PR3355

If you are working in Khan Academy's frontend, you can run the below command.

./dev/tools/bump_perseus_version.ts -t PR3355

If you are working in Khan Academy's webapp, you can run the below command.

./dev/tools/bump_perseus_version.js -t PR3355

Copy link
Copy Markdown
Contributor

@SonicScrewdriver SonicScrewdriver left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already approved earlier :)

Comment on lines 170 to +184

if (first) {
pickup.current = vec.transform(point, userTransform);
dragStarted.current = false;
}
if (vec.mag(pixelMovement) === 0) {
return;
}
// Don't start a drag if no movement; a click with zero
// movement should not set isCurrentlyDragging and block
// subsequent click-to-add-point events.
if (!dragStarted.current) {
dragStarted.current = true;
onDragStart?.();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Race condition: moving setIsCurrentlyDragging(true) from onMove to onDragStart loses the self-correction that protected against a stale dragEndCallbackTimer resetting the flag mid-drag. If a user clicks a point (no movement) and then starts dragging another point within 400ms, the stale timer fires and sets isCurrentlyDragging to false during the active drag, defeating the iOS phantom click protection. Consider clearing the timer in onDragStart (e.g., dragEndCallbackTimer.clear()) to prevent the stale callback from interfering.

Extended reasoning...

What the bug is

The PR moves setIsCurrentlyDragging(true) from onMove (called every animation frame during a drag) to the new onDragStart callback (called only once when a drag begins). This introduces a race condition with the 400ms dragEndCallbackTimer used to block phantom clicks on iOS Safari.

The asymmetry that enables the race

In use-draggable.ts, onDragEnd is called at line 167-168 when last is true — this happens on mouseup before the zero-movement early return at line 175-176. However, onDragStart is called at line 181-183 after the zero-movement guard. This means a click on a point with no mouse movement triggers onDragEnd (starting the 400ms timer) but does not trigger onDragStart. The PR's own test "does not call onDragStart on a click with no mouse movement" confirms this intentional asymmetry.

Step-by-step proof

  1. t=0ms: User clicks/taps on point A with no mouse movement. useDrag fires with last=true and pixelMovement=[0,0]. At line 167, onDragEnd() is called, which calls dragEndCallbackTimer.set() starting a 400ms timer to set isCurrentlyDragging = false. The zero-movement check at line 175 causes an early return, so onDragStart is never called.

  2. t=200ms: User begins dragging point B. useDrag fires with first=true and then with actual movement. At line 181-183, onDragStart() fires, calling setIsCurrentlyDragging(true). The drag is now in progress.

  3. t=400ms: The stale timer from step 1 fires, calling setIsCurrentlyDragging(false). The drag on point B is still active, but the flag is now incorrectly false.

  4. t=500ms+: User releases point B. On iOS Safari, a phantom click event fires at the original touch location. The onClick handler on the <rect> checks isCurrentlyDragging (line 133 in point.tsx), finds it false, and adds an unwanted new point.

Why the old code did not have this problem

In the old code, setIsCurrentlyDragging(true) was called inside onMove, which fires on every animation frame (~60fps). Even if the stale timer fired mid-drag, the very next onMove call (~16ms later) would reset isCurrentlyDragging back to true, providing continuous self-correction. The new code calls onDragStart only once, so there is no self-correction mechanism.

Impact and fix

This affects both UnlimitedPointGraph (point.tsx:157) and UnlimitedPolygonGraph (polygon.tsx:557), which both use the same onDragStart + onDragEnd + dragEndCallbackTimer pattern. The timing window is narrow (click a point then start dragging within 400ms) and only manifests on iOS Safari where phantom clicks occur, but it is a regression introduced by this PR. The simplest fix would be to clear the timer when onDragStart fires in both point.tsx and polygon.tsx. For example: onDragStart={() => { dragEndCallbackTimer.clear(); setIsCurrentlyDragging(true); }}.

@SonicScrewdriver SonicScrewdriver merged commit 4da6a2e into main Mar 16, 2026
23 of 29 checks passed
@SonicScrewdriver SonicScrewdriver deleted the deploy/jon branch March 16, 2026 15:25
SonicScrewdriver added a commit that referenced this pull request Mar 16, 2026
…ragEndCallbackTimer when starting a new drag (#3363)

## Summary:
Clears any pending `dragEndCallbackTimer` when a new drag begins in the unlimited point and polygon graphs, preventing a stale timer (started by a stationary click) from resetting `isCurrentlyDragging` mid-drag and defeating the iOS Safari phantom click protection introduced in #2895.

This issue was called out by a late [Claude Code review on another PR](#3355 (comment)) that was unfortunately missed by myself.

## Test plan:
- Tests pass

Author: SonicScrewdriver

Reviewers: jonmarkprice, claude[bot]

Required Reviewers:

Approved By: jonmarkprice

Checks: ✅ 10 checks were successful, ⏭️  1 check has been skipped, ⚪️ 1 check is neutral

Pull Request URL: #3363
catandthemachines pushed a commit that referenced this pull request Apr 1, 2026
…ragEndCallbackTimer when starting a new drag (#3363)

## Summary:
Clears any pending `dragEndCallbackTimer` when a new drag begins in the unlimited point and polygon graphs, preventing a stale timer (started by a stationary click) from resetting `isCurrentlyDragging` mid-drag and defeating the iOS Safari phantom click protection introduced in #2895.

This issue was called out by a late [Claude Code review on another PR](#3355 (comment)) that was unfortunately missed by myself.

## Test plan:
- Tests pass

Author: SonicScrewdriver

Reviewers: jonmarkprice, claude[bot]

Required Reviewers:

Approved By: jonmarkprice

Checks: ✅ 10 checks were successful, ⏭️  1 check has been skipped, ⚪️ 1 check is neutral

Pull Request URL: #3363
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants