Skip to content

Commit a99f260

Browse files
authored
fix(feedback): Smoother cropping experience and better UI (#11165)
Fixes some cropping jank: 1. Cropping is smoother: previously the event listeners weren't working when the mouse was on the cropping buttons, which was causing the jankiness 2. Changes the cursor when on the cropping buttons 3. Increased the minimum size required for cropping so it's easier to resize https://github.com/getsentry/sentry-javascript/assets/55311782/71b7401e-c9f2-44c2-9f92-64c1c80d24ef Fixes getsentry/sentry#67056
1 parent 4ba3e40 commit a99f260

File tree

1 file changed

+49
-37
lines changed

1 file changed

+49
-37
lines changed

packages/feedback/src/screenshot/components/ScreenshotEditor.tsx

Lines changed: 49 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// eslint-disable max-lines
1+
/* eslint-disable max-lines */
22
import type { ComponentType, VNode, h as hType } from 'preact';
33
// biome-ignore lint: needed for preact
44
import { h } from 'preact'; // eslint-disable-line @typescript-eslint/no-unused-vars
@@ -8,6 +8,10 @@ import type { Dialog } from '../../types';
88
import { createScreenshotInputStyles } from './ScreenshotInput.css';
99
import { useTakeScreenshot } from './useTakeScreenshot';
1010

11+
const CROP_BUTTON_SIZE = 30;
12+
const CROP_BUTTON_BORDER = 3;
13+
const CROP_BUTTON_OFFSET = CROP_BUTTON_SIZE + CROP_BUTTON_BORDER;
14+
1115
interface FactoryParams {
1216
h: typeof hType;
1317
imageBuffer: HTMLCanvasElement;
@@ -19,10 +23,10 @@ interface Props {
1923
}
2024

2125
interface Box {
22-
startx: number;
23-
starty: number;
24-
endx: number;
25-
endy: number;
26+
startX: number;
27+
startY: number;
28+
endX: number;
29+
endY: number;
2630
}
2731

2832
interface Rect {
@@ -34,10 +38,10 @@ interface Rect {
3438

3539
const constructRect = (box: Box): Rect => {
3640
return {
37-
x: Math.min(box.startx, box.endx),
38-
y: Math.min(box.starty, box.endy),
39-
width: Math.abs(box.startx - box.endx),
40-
height: Math.abs(box.starty - box.endy),
41+
x: Math.min(box.startX, box.endX),
42+
y: Math.min(box.startY, box.endY),
43+
width: Math.abs(box.startX - box.endX),
44+
height: Math.abs(box.startY - box.endY),
4145
};
4246
};
4347

@@ -51,7 +55,7 @@ const getContainedSize = (img: HTMLCanvasElement): Box => {
5155
}
5256
const x = (img.clientWidth - width) / 2;
5357
const y = (img.clientHeight - height) / 2;
54-
return { startx: x, starty: y, endx: width + x, endy: height + y };
58+
return { startX: x, startY: y, endX: width + x, endY: height + y };
5559
};
5660

5761
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -62,7 +66,7 @@ export function makeScreenshotEditorComponent({ h, imageBuffer, dialog }: Factor
6266
const canvasContainerRef = useRef<HTMLDivElement>(null);
6367
const cropContainerRef = useRef<HTMLDivElement>(null);
6468
const croppingRef = useRef<HTMLCanvasElement>(null);
65-
const [croppingRect, setCroppingRect] = useState<Box>({ startx: 0, starty: 0, endx: 0, endy: 0 });
69+
const [croppingRect, setCroppingRect] = useState<Box>({ startX: 0, startY: 0, endX: 0, endY: 0 });
6670
const [confirmCrop, setConfirmCrop] = useState(false);
6771

6872
useEffect(() => {
@@ -85,7 +89,7 @@ export function makeScreenshotEditorComponent({ h, imageBuffer, dialog }: Factor
8589
cropButton.style.top = `${imageDimensions.y}px`;
8690
}
8791

88-
setCroppingRect({ startx: 0, starty: 0, endx: imageDimensions.width, endy: imageDimensions.height });
92+
setCroppingRect({ startX: 0, startY: 0, endX: imageDimensions.width, endY: imageDimensions.height });
8993
}
9094

9195
useEffect(() => {
@@ -118,44 +122,51 @@ export function makeScreenshotEditorComponent({ h, imageBuffer, dialog }: Factor
118122
setConfirmCrop(false);
119123
const handleMouseMove = makeHandleMouseMove(corner);
120124
const handleMouseUp = (): void => {
121-
croppingRef.current && croppingRef.current.removeEventListener('mousemove', handleMouseMove);
125+
DOCUMENT.removeEventListener('mousemove', handleMouseMove);
122126
DOCUMENT.removeEventListener('mouseup', handleMouseUp);
123127
setConfirmCrop(true);
124128
};
125129

126130
DOCUMENT.addEventListener('mouseup', handleMouseUp);
127-
croppingRef.current && croppingRef.current.addEventListener('mousemove', handleMouseMove);
131+
DOCUMENT.addEventListener('mousemove', handleMouseMove);
128132
}
129133

130134
const makeHandleMouseMove = useCallback((corner: string) => {
131135
return function (e: MouseEvent) {
136+
if (!croppingRef.current) {
137+
return;
138+
}
139+
const cropCanvas = croppingRef.current;
140+
const cropBoundingRect = cropCanvas.getBoundingClientRect();
141+
const mouseX = e.clientX - cropBoundingRect.x;
142+
const mouseY = e.clientY - cropBoundingRect.y;
132143
switch (corner) {
133144
case 'topleft':
134145
setCroppingRect(prev => ({
135146
...prev,
136-
startx: Math.min(e.offsetX, prev.endx - 30),
137-
starty: Math.min(e.offsetY, prev.endy - 30),
147+
startX: Math.min(Math.max(0, mouseX), prev.endX - CROP_BUTTON_OFFSET),
148+
startY: Math.min(Math.max(0, mouseY), prev.endY - CROP_BUTTON_OFFSET),
138149
}));
139150
break;
140151
case 'topright':
141152
setCroppingRect(prev => ({
142153
...prev,
143-
endx: Math.max(e.offsetX, prev.startx + 30),
144-
starty: Math.min(e.offsetY, prev.endy - 30),
154+
endX: Math.max(Math.min(mouseX, cropCanvas.width), prev.startX + CROP_BUTTON_OFFSET),
155+
startY: Math.min(Math.max(0, mouseY), prev.endY - CROP_BUTTON_OFFSET),
145156
}));
146157
break;
147158
case 'bottomleft':
148159
setCroppingRect(prev => ({
149160
...prev,
150-
startx: Math.min(e.offsetX, prev.endx - 30),
151-
endy: Math.max(e.offsetY, prev.starty + 30),
161+
startX: Math.min(Math.max(0, mouseX), prev.endX - CROP_BUTTON_OFFSET),
162+
endY: Math.max(Math.min(mouseY, cropCanvas.height), prev.startY + CROP_BUTTON_OFFSET),
152163
}));
153164
break;
154165
case 'bottomright':
155166
setCroppingRect(prev => ({
156167
...prev,
157-
endx: Math.max(e.offsetX, prev.startx + 30),
158-
endy: Math.max(e.offsetY, prev.starty + 30),
168+
endX: Math.max(Math.min(mouseX, cropCanvas.width), prev.startX + CROP_BUTTON_OFFSET),
169+
endY: Math.max(Math.min(mouseY, cropCanvas.height), prev.startY + CROP_BUTTON_OFFSET),
159170
}));
160171
break;
161172
}
@@ -229,33 +240,33 @@ export function makeScreenshotEditorComponent({ h, imageBuffer, dialog }: Factor
229240
<div class="cropButtonContainer" style={{ position: 'absolute' }} ref={cropContainerRef}>
230241
<canvas style={{ position: 'absolute' }} ref={croppingRef}></canvas>
231242
<CropCorner
232-
left={croppingRect.startx}
233-
top={croppingRect.starty}
243+
left={croppingRect.startX}
244+
top={croppingRect.startY}
234245
onGrabButton={onGrabButton}
235246
corner="topleft"
236247
></CropCorner>
237248
<CropCorner
238-
left={croppingRect.endx - 30}
239-
top={croppingRect.starty}
249+
left={croppingRect.endX - CROP_BUTTON_SIZE}
250+
top={croppingRect.startY}
240251
onGrabButton={onGrabButton}
241252
corner="topright"
242253
></CropCorner>
243254
<CropCorner
244-
left={croppingRect.startx}
245-
top={croppingRect.endy - 30}
255+
left={croppingRect.startX}
256+
top={croppingRect.endY - CROP_BUTTON_SIZE}
246257
onGrabButton={onGrabButton}
247258
corner="bottomleft"
248259
></CropCorner>
249260
<CropCorner
250-
left={croppingRect.endx - 30}
251-
top={croppingRect.endy - 30}
261+
left={croppingRect.endX - CROP_BUTTON_SIZE}
262+
top={croppingRect.endY - CROP_BUTTON_SIZE}
252263
onGrabButton={onGrabButton}
253264
corner="bottomright"
254265
></CropCorner>
255266
<div
256267
style={{
257-
left: Math.max(0, croppingRect.endx - 191),
258-
top: Math.max(0, croppingRect.endy + 8),
268+
left: Math.max(0, croppingRect.endX - 191),
269+
top: Math.max(0, croppingRect.endY + 8),
259270
display: confirmCrop ? 'flex' : 'none',
260271
}}
261272
class="crop-btn-group"
@@ -265,10 +276,10 @@ export function makeScreenshotEditorComponent({ h, imageBuffer, dialog }: Factor
265276
e.preventDefault();
266277
if (croppingRef.current) {
267278
setCroppingRect({
268-
startx: 0,
269-
starty: 0,
270-
endx: croppingRef.current.width,
271-
endy: croppingRef.current.height,
279+
startX: 0,
280+
startY: 0,
281+
endX: croppingRef.current.width,
282+
endY: croppingRef.current.height,
272283
});
273284
}
274285
setConfirmCrop(false);
@@ -316,7 +327,8 @@ function CropCorner({
316327
borderLeft: corner === 'topleft' || corner === 'bottomleft' ? 'solid purple' : 'none',
317328
borderRight: corner === 'topright' || corner === 'bottomright' ? 'solid purple' : 'none',
318329
borderBottom: corner === 'bottomleft' || corner === 'bottomright' ? 'solid purple' : 'none',
319-
borderWidth: '3px',
330+
borderWidth: `${CROP_BUTTON_BORDER}px`,
331+
cursor: corner === 'topleft' || corner === 'bottomright' ? 'nwse-resize' : 'nesw-resize',
320332
}}
321333
onMouseDown={e => {
322334
e.preventDefault();

0 commit comments

Comments
 (0)