fix: make zoom in the diff dialog obey the cursor and stay inside the dialog - #383
Merged
nGervasyuk merged 5 commits intoJul 27, 2026
Conversation
Zoom was handled on the Konva stage, which is the element being scaled. Zooming out shrinks it out from under the pointer, after which the wheel reaches the browser instead and pinching zooms the whole page - the only way back is resetting the browser zoom. Move the listener to the surrounding pane, which keeps its size, clamp the scale, and keep the point under the cursor in place instead of scaling from the top left corner, which is what made it so easy to lose the image in the first place. React 17 registers onWheel passively, so the listener is attached manually.
The toolbar buttons scaled the stage directly, so Zoom Out could shrink the image to nothing while the wheel stopped at the floor. Move the bounds into the scale helper and clamp both paths through it.
Drives a real pointer instead of a synthetic event: shrinks the image until it no longer reaches the far side of the pane, then zooms from the empty half. With the listener back on the canvas the width does not budge.
Every wheel event applied its whole step immediately, so a gesture ended as abruptly as it started. Accumulate the step into a target scale and chase it a share at a time on animation frames, which coasts to a stop. The cursor anchor is resolved once per gesture so the point under the pointer stays put for the whole animation.
Ease over elapsed time rather than a fixed share per frame, so the gesture lasts as long on a 120Hz display as on a 60Hz one, and stretch the coast a little. While the animation drives the scroll position, ignore the scroll events it causes - echoing them back into state rendered the pane twice per frame.
|
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 & why
Zooming in the fullscreen approve/reject dialog can escape into the browser. Zoom out a little with the pointer away from the centre and the next scroll zooms the whole page rather than the screenshot; the only way back is resetting the browser zoom.
Two things combine to cause it:
<Stage>is CSS-scaled withtransform: scale(...), so zooming out physically shrinks the canvas. Once it slips out from under the pointer, the wheel no longer reaches Konva, nothing callspreventDefault(), and the browser handles the gesture — with a trackpad pinch that is page zoom.transformOrigin: "top left". Zoom does not follow the cursor, so the image runs toward the top-left corner as it shrinks. That is what puts the pointer off the canvas within a couple of gestures, which makes (1) easy to hit.Everything below is the same gesture in the same dialog, so it comes as one change.
Changes
onWheelpassively, sopreventDefault()from JSX is silently ignored; the listener is attached with{ passive: false }instead.scale.helper, and the toolbar Zoom In/Out buttons go through the same clamp. They previously scaled the stage directly, so Zoom Out could shrink the image to zero width even though the wheel stopped at the floor.ZOOM_EASINGis the single knob: higher is snappier, lower coasts longer.Drag/pan state is untouched.
Testing
Five Playwright tests, each verified to fail without the part of the change it covers:
defaultPrevented === false306.2→306.2)scrollLeft === 00widthThe cursor-anchoring test asserts the horizontal axis only: the pane grows in height rather than scrolling, so there is no vertical scroll to check.
Playwright 26/26,
tsc --noEmit,eslint,prettierandvite buildclean on changed files. No snapshot change — the default scale is unaffected.Screenshots
Screen.Recording.2026-07-27.at.17.23.22.mov
The easing constant is a matter of taste; happy to retune it.