Stop the wheel on a Bot's screen from also scrolling the page around it - #603
Merged
davidmckayv merged 2 commits intoSep 18, 2026
Merged
Conversation
LiveScreen forwarded the wheel from React's onWheel and called preventDefault there, so that scrolling the Bot's page would not also scroll this one. React attaches wheel to its root as a passive listener (react-dom's addTrappedEventListener, for touchstart, touchmove and wheel whenever the browser supports passive listeners), and a browser ignores preventDefault in a passive listener. Chrome says so in the console: "Unable to preventDefault inside passive event listener invocation." So while somebody drove a Bot's browser, the wheel reached the Bot's page and also scrolled whatever on this page was under it. In the expanded view that includes the max-h-[75vh] overflow-auto frame holding the screen, and Ctrl with the wheel zoomed the app. The wheel is now handled by a listener on the canvas itself, added with passive: false while driving, and removed when driving stops. What it sends is unchanged. `at` now takes anything with clientX and clientY, so a native event can be passed to it as well as React's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kevin9327
requested review from
MikeRyanDev,
davidmckayv,
guidovizoso,
mxmzb and
tylerslaton
as code owners
September 18, 2026 10:32
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
# Conflicts: # CHANGELOG.md
davidmckayv
approved these changes
Sep 18, 2026
davidmckayv
left a comment
Contributor
There was a problem hiding this comment.
Code-verified clean; CI green on this sha.
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.
What this changes
While somebody drives a Bot's browser,
LiveScreen(app/src/components/computer/live-screen.tsx) forwards a turn of the mouse wheel over the screen, and callsevent.preventDefault()so the wheel does not also act on the app. That call was made in React'sonWheel, and it does nothing there.React attaches its
wheelhandler to the root as a passive listener. Inreact-dom19.2 (addTrappedEventListener),touchstart,touchmoveandwheelare registered with{ passive: true }whenever the browser supports passive listeners, which every current browser does. A browser ignorespreventDefaultin a passive listener, and Chrome logs a warning each time:So the wheel reached the Bot's page and also acted on this one:
max-h-[75vh] overflow-autoincomputer-view.tsx. Once the screen is taller than that, the screen scrolls away under the pointer while the person is scrolling the Bot's page.After this change, the wheel is handled by a listener on the canvas itself, added in an effect with
{ passive: false }while driving and removed when driving stops. It sends the same message as before. TheonWheelprop is removed, so a turn is sent once.atnow accepts any{ clientX, clientY }rather than only a React mouse event, so the native event can be passed to it. The mouse buttons, context menu and keyboard are untouched.Where it runs
wheellistener on the canvas replaces React'sonWheel, and it only exists while driving.Boundary and audit
wheelinput as before.Changelog
CHANGELOG.mdunderUnreleased.Proof
In Chromium, with React 19.2.0: a page with two
overflow: autoframes, each holding a taller canvas. One canvas hasonWheel={(e) => e.preventDefault()}, the same pattern asmain. The other has a nativewheellistener added with{ passive: false }, the pattern this PR uses. I sent real wheel input over each through the Browser pane:In the test suite, happy-dom hides the problem: under it, React does not detect passive support and binds
wheelactively, so apreventDefaultfromonWheeldoes work there. For that reason the new test inapp/tests/live-screen-mouse.test.tsx,a turn of the wheel is stopped here as well as sent there, checks the listener the canvas itself holds. It records the options of everywheellistener added to a canvas and expects exactly[{ passive: false }]. It then dispatches a wheel on the canvas and expects the event to be prevented and exactly onewheelmessage to be sent. It uses aMouseEventof typewheel, because happy-dom'sWheelEventhas no coordinates.With
live-screen.tsxfrommain(only the test added):With the change, 5 pass, and so do the 9 tests in
live-screen-keyboard.test.tsx.bun test --coverageshows every line of the new effect executed, including its cleanup. App suite (bun test app, Windows): 890 pass, 29 fail, the same 29 environment failuresmainhas on this machine.bun run typecheckinapp/is clean, andbiome checkpasses for both files.This touches the same file as #602, in a different place. The two branches merge cleanly together except for
CHANGELOG.md, like every open PR here. I'm happy to rebase whichever lands second.On Linux too (
ubuntu-latest, Bun 1.3.14; a throwaway workflow on my fork, since fork PRs get no CI here):bun test appis 925 pass, 0 fail with all five of my open PRs merged together, andformat:check,lintandtypecheckare green.🤖 Generated with Claude Code