Skip to content

Fix globe drag not ending when pointer released outside element#1447

Merged
N2D4 merged 1 commit into
devfrom
devin/1779244766-fix-globe-drag
May 20, 2026
Merged

Fix globe drag not ending when pointer released outside element#1447
N2D4 merged 1 commit into
devfrom
devin/1779244766-fix-globe-drag

Conversation

@N2D4
Copy link
Copy Markdown
Contributor

@N2D4 N2D4 commented May 20, 2026

Fixes the overview globe continuing to rotate when re-entering it after releasing the mouse button outside. Listens for pointerup on window and forwards it to the canvas when it originated outside the globe element.

Link to Devin session: https://app.devin.ai/sessions/a1b15005ef104bbe8d77b54ace75f66f
Requested by: @N2D4


Note

Low Risk
Low risk UI interaction fix that only adds a window-level pointerup listener to ensure OrbitControls ends drags correctly; main concern is unintended event forwarding if the target detection is wrong.

Overview
Fixes the overview globe getting “stuck” rotating after a drag when the pointer is released outside the globe canvas.

Adds a useEffect that listens for pointerup on window and, when the event originated outside the canvas, dispatches a synthetic pointerup to the globe renderer element so OrbitControls reliably terminates the drag state.

Reviewed by Cursor Bugbot for commit 714f60d. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

Bug Fixes

  • Fixed globe dragging behavior to properly handle pointer release events when the cursor exits the canvas area, preventing drag operations from stalling and improving interaction reliability.

Review Change Stack

When dragging the globe to rotate it and releasing the mouse button
outside the globe element, OrbitControls could miss the pointerup event
(pointer capture can be lost intermittently). This caused the globe to
continue rotating on re-entry even without the mouse button held.

Fix: listen for pointerup on window and forward it to the canvas when
the event originated outside the globe, so OrbitControls properly ends
the drag.

Co-Authored-By: Konstantin Wohlwend <n2d4xc@gmail.com>
@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@vercel
Copy link
Copy Markdown

vercel Bot commented May 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stack-auth-hosted-components Ready Ready Preview, Comment May 20, 2026 2:48am
stack-auth-mcp Ready Ready Preview, Comment May 20, 2026 2:48am
stack-auth-skills Ready Ready Preview, Comment May 20, 2026 2:48am
stack-backend Ready Ready Preview, Comment May 20, 2026 2:48am
stack-dashboard Ready Ready Preview, Comment May 20, 2026 2:48am
stack-demo Ready Ready Preview, Comment May 20, 2026 2:48am
stack-docs Ready Ready Preview, Comment May 20, 2026 2:48am
stack-preview-backend Ready Ready Preview, Comment May 20, 2026 2:48am
stack-preview-dashboard Ready Ready Preview, Comment May 20, 2026 2:48am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 076b3ee1-b600-4b0b-9830-4ab7f5e32186

📥 Commits

Reviewing files that changed from the base of the PR and between 954ebff and 714f60d.

📒 Files selected for processing (1)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx

📝 Walkthrough

Walkthrough

A useEffect hook is added to the globe component that listens for global pointer release events on the window. When a pointer is released outside the canvas, the effect synthesizes and dispatches a pointerup event onto the globe canvas to ensure OrbitControls correctly terminates drag operations regardless of where the pointer is released.

Changes

Globe Pointer Release Forwarding

Layer / File(s) Summary
Globe pointer release forwarding
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx
A useEffect is added that registers a global pointerup listener on window. When the pointer is released, the effect checks if the release occurred outside the canvas bounds and, if so, dispatches a synthetic bubbled pointerup event onto the canvas element with the original pointerId and pointerType preserved. The listener is cleaned up on component unmount; the effect is gated on globeReady to prevent listener registration before the canvas is ready.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Poem

🐰 A pointer lost in the digital night,
Now finds its way back to the canvas bright!
Outside the globe, we capture the release,
And forward it home—drag termination's peace. ✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch devin/1779244766-fix-globe-drag

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@N2D4 N2D4 marked this pull request as ready for review May 20, 2026 06:09
Copilot AI review requested due to automatic review settings May 20, 2026 06:09
@N2D4 N2D4 merged commit 97f86a1 into dev May 20, 2026
31 of 38 checks passed
@N2D4 N2D4 deleted the devin/1779244766-fix-globe-drag branch May 20, 2026 06:09
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 20, 2026

Greptile Summary

This PR fixes the overview globe continuing to rotate after the mouse button is released outside the canvas element. It adds a useEffect that attaches a pointerup listener on window and forwards a synthetic pointerup to the canvas's DOM element whenever the release occurs outside of it, ensuring OrbitControls always receives the drag-end signal.

  • A window-level pointerup handler is registered once globeReady is true, guarded by a domElement null check, and properly cleaned up in the effect teardown.
  • The synthetic PointerEvent forwards pointerId and pointerType so OrbitControls can match the event to the correct tracked pointer; however, bubbles: true is set unnecessarily (OrbitControls listens directly on domElement) and causes the event to re-trigger intermediate DOM listeners.

Confidence Score: 4/5

Safe to merge; the change is well-scoped and its cleanup path is correct.

The fix correctly addresses the drag-not-ending bug: the target check prevents duplicate dispatches when pointer capture is still active, pointerId and pointerType are forwarded so OrbitControls matches the right pointer, and the effect teardown removes the listener when the component unmounts. The only notable issue is bubbles: true on the synthetic event, which is unnecessary and propagates the event through intermediate DOM listeners after OrbitControls handles it.

Only globe.tsx changed; no other files require attention.

Important Files Changed

Filename Overview
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx Adds a window-level pointerup listener that forwards the event to the canvas DOM element when pointer capture is lost while the pointer is outside the canvas, fixing lingering drag state in OrbitControls.

Sequence Diagram

sequenceDiagram
    participant User
    participant Window
    participant Handler as handleWindowPointerUp
    participant Canvas as domElement (canvas)
    participant OC as OrbitControls

    User->>Window: pointerup (outside canvas)
    Window->>Handler: fires listener
    Handler->>Handler: "check e.target !== domElement?"
    alt pointer released outside canvas
        Handler->>Canvas: dispatchEvent(PointerEvent 'pointerup')
        Canvas->>OC: onPointerUp fires
        OC->>OC: removePointer / reset drag state
    else pointer released on canvas (normal path)
        Handler-->>Handler: no-op (target check fails)
    end
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx:1038-1042
`bubbles: true` is not needed here and has a subtle downside. OrbitControls attaches its `pointerup` listener directly on `domElement` (not a parent), so bubbling isn't required for it to fire. With `bubbles: true`, the synthetic event propagates up through the entire DOM after OrbitControls handles it, triggering any intermediate `pointerup` listeners (e.g. React synthetic event delegation at `document`) and re-entering `handleWindowPointerUp` on `window` — where it's a no-op due to the target check, but still an unnecessary invocation. Using `bubbles: false` keeps the dispatch scoped to the canvas and avoids unexpected side-effects.

```suggestion
        domElement.dispatchEvent(new PointerEvent('pointerup', {
          pointerId: e.pointerId,
          pointerType: e.pointerType,
          bubbles: false,
        }));
```

Reviews (1): Last reviewed commit: "Fix globe drag not ending when pointer r..." | Re-trigger Greptile

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 714f60d. Configure here.

return () => {
window.removeEventListener('pointerup', handleWindowPointerUp);
};
}, [globeReady]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale canvas in pointerup handler

Medium Severity

The window pointerup listener keeps the renderer canvas from when the effect first ran. globeReady stays true when Globe remounts (theme or error refresh), so synthetic pointerup events go to a detached canvas and the live controls may still miss an outside release.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 714f60d. Configure here.

Comment on lines +1038 to +1042
domElement.dispatchEvent(new PointerEvent('pointerup', {
pointerId: e.pointerId,
pointerType: e.pointerType,
bubbles: true,
}));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 bubbles: true is not needed here and has a subtle downside. OrbitControls attaches its pointerup listener directly on domElement (not a parent), so bubbling isn't required for it to fire. With bubbles: true, the synthetic event propagates up through the entire DOM after OrbitControls handles it, triggering any intermediate pointerup listeners (e.g. React synthetic event delegation at document) and re-entering handleWindowPointerUp on window — where it's a no-op due to the target check, but still an unnecessary invocation. Using bubbles: false keeps the dispatch scoped to the canvas and avoids unexpected side-effects.

Suggested change
domElement.dispatchEvent(new PointerEvent('pointerup', {
pointerId: e.pointerId,
pointerType: e.pointerType,
bubbles: true,
}));
domElement.dispatchEvent(new PointerEvent('pointerup', {
pointerId: e.pointerId,
pointerType: e.pointerType,
bubbles: false,
}));
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx
Line: 1038-1042

Comment:
`bubbles: true` is not needed here and has a subtle downside. OrbitControls attaches its `pointerup` listener directly on `domElement` (not a parent), so bubbling isn't required for it to fire. With `bubbles: true`, the synthetic event propagates up through the entire DOM after OrbitControls handles it, triggering any intermediate `pointerup` listeners (e.g. React synthetic event delegation at `document`) and re-entering `handleWindowPointerUp` on `window` — where it's a no-op due to the target check, but still an unnecessary invocation. Using `bubbles: false` keeps the dispatch scoped to the canvas and avoids unexpected side-effects.

```suggestion
        domElement.dispatchEvent(new PointerEvent('pointerup', {
          pointerId: e.pointerId,
          pointerType: e.pointerType,
          bubbles: false,
        }));
```

How can I resolve this? If you propose a fix, please make it concise.

@N2D4 N2D4 review requested due to automatic review settings May 20, 2026 06:30
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.

1 participant