Skip to content

Commit 8f4f3d8

Browse files
authored
feat(feedback): Update user feedback screenshot and cropping to align with designs (#11227)
Updated some screenshot styles to align with design: - Changed cropping to more closely follow photoshop: darker background, white cropping corners + rectangle and additional black border for contrast if the screenshot is white - Changed naming to follow BES naming - Added padding so that the submit/cancel buttons are always visible without scrolling - Reduced blurriness of cropping rectangle <img width="1270" alt="image" src="https://github.com/getsentry/sentry-javascript/assets/55311782/df73a537-58f9-4f03-9edc-403039e61122"> Relates to getsentry/sentry#63749 ---------
1 parent 16ea3be commit 8f4f3d8

File tree

3 files changed

+76
-50
lines changed

3 files changed

+76
-50
lines changed

packages/feedback/src/constants/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,5 @@ export const FEEDBACK_WIDGET_SOURCE = 'widget';
2626
export const FEEDBACK_API_SOURCE = 'api';
2727

2828
export const SUCCESS_MESSAGE_TIMEOUT = 5000;
29+
30+
export const CROP_COLOR = '#ffffff';

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

Lines changed: 42 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ 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
55
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
6-
import { DOCUMENT, WINDOW } from '../../constants';
6+
import { CROP_COLOR, DOCUMENT, WINDOW } from '../../constants';
77
import type { Dialog } from '../../types';
88
import { createScreenshotInputStyles } from './ScreenshotInput.css';
99
import { useTakeScreenshot } from './useTakeScreenshot';
1010

1111
const CROP_BUTTON_SIZE = 30;
1212
const CROP_BUTTON_BORDER = 3;
1313
const CROP_BUTTON_OFFSET = CROP_BUTTON_SIZE + CROP_BUTTON_BORDER;
14+
const DPI = WINDOW.devicePixelRatio;
1415

1516
interface FactoryParams {
1617
h: typeof hType;
@@ -79,8 +80,14 @@ export function makeScreenshotEditorComponent({ h, imageBuffer, dialog }: Factor
7980
const cropper = croppingRef.current;
8081
const imageDimensions = constructRect(getContainedSize(imageBuffer));
8182
if (cropper) {
82-
cropper.width = imageDimensions.width;
83-
cropper.height = imageDimensions.height;
83+
cropper.width = imageDimensions.width * DPI;
84+
cropper.height = imageDimensions.height * DPI;
85+
cropper.style.width = `${imageDimensions.width}px`;
86+
cropper.style.height = `${imageDimensions.height}px`;
87+
const ctx = cropper.getContext('2d');
88+
if (ctx) {
89+
ctx.scale(DPI, DPI);
90+
}
8491
}
8592

8693
const cropButton = cropContainerRef.current;
@@ -104,9 +111,9 @@ export function makeScreenshotEditorComponent({ h, imageBuffer, dialog }: Factor
104111
if (!ctx) {
105112
return;
106113
}
114+
107115
const imageDimensions = constructRect(getContainedSize(imageBuffer));
108116
const croppingBox = constructRect(croppingRect);
109-
110117
ctx.clearRect(0, 0, imageDimensions.width, imageDimensions.height);
111118

112119
// draw gray overlay around the selection
@@ -115,9 +122,12 @@ export function makeScreenshotEditorComponent({ h, imageBuffer, dialog }: Factor
115122
ctx.clearRect(croppingBox.x, croppingBox.y, croppingBox.width, croppingBox.height);
116123

117124
// draw selection border
118-
ctx.strokeStyle = 'purple';
125+
ctx.strokeStyle = CROP_COLOR;
119126
ctx.lineWidth = 3;
120-
ctx.strokeRect(croppingBox.x, croppingBox.y, croppingBox.width, croppingBox.height);
127+
ctx.strokeRect(croppingBox.x + 1, croppingBox.y + 1, croppingBox.width - 2, croppingBox.height - 2);
128+
ctx.strokeStyle = '#000000';
129+
ctx.lineWidth = 1;
130+
ctx.strokeRect(croppingBox.x + 3, croppingBox.y + 3, croppingBox.width - 6, croppingBox.height - 6);
121131
}, [croppingRect]);
122132

123133
function onGrabButton(e: Event, corner: string): void {
@@ -143,32 +153,32 @@ export function makeScreenshotEditorComponent({ h, imageBuffer, dialog }: Factor
143153
const mouseX = e.clientX - cropBoundingRect.x;
144154
const mouseY = e.clientY - cropBoundingRect.y;
145155
switch (corner) {
146-
case 'topleft':
156+
case 'top-left':
147157
setCroppingRect(prev => ({
148158
...prev,
149159
startX: Math.min(Math.max(0, mouseX), prev.endX - CROP_BUTTON_OFFSET),
150160
startY: Math.min(Math.max(0, mouseY), prev.endY - CROP_BUTTON_OFFSET),
151161
}));
152162
break;
153-
case 'topright':
163+
case 'top-right':
154164
setCroppingRect(prev => ({
155165
...prev,
156-
endX: Math.max(Math.min(mouseX, cropCanvas.width), prev.startX + CROP_BUTTON_OFFSET),
166+
endX: Math.max(Math.min(mouseX, cropCanvas.width / DPI), prev.startX + CROP_BUTTON_OFFSET),
157167
startY: Math.min(Math.max(0, mouseY), prev.endY - CROP_BUTTON_OFFSET),
158168
}));
159169
break;
160-
case 'bottomleft':
170+
case 'bottom-left':
161171
setCroppingRect(prev => ({
162172
...prev,
163173
startX: Math.min(Math.max(0, mouseX), prev.endX - CROP_BUTTON_OFFSET),
164-
endY: Math.max(Math.min(mouseY, cropCanvas.height), prev.startY + CROP_BUTTON_OFFSET),
174+
endY: Math.max(Math.min(mouseY, cropCanvas.height / DPI), prev.startY + CROP_BUTTON_OFFSET),
165175
}));
166176
break;
167-
case 'bottomright':
177+
case 'bottom-right':
168178
setCroppingRect(prev => ({
169179
...prev,
170-
endX: Math.max(Math.min(mouseX, cropCanvas.width), prev.startX + CROP_BUTTON_OFFSET),
171-
endY: Math.max(Math.min(mouseY, cropCanvas.height), prev.startY + CROP_BUTTON_OFFSET),
180+
endX: Math.max(Math.min(mouseX, cropCanvas.width / DPI), prev.startX + CROP_BUTTON_OFFSET),
181+
endY: Math.max(Math.min(mouseY, cropCanvas.height / DPI), prev.startY + CROP_BUTTON_OFFSET),
172182
}));
173183
break;
174184
}
@@ -238,40 +248,40 @@ export function makeScreenshotEditorComponent({ h, imageBuffer, dialog }: Factor
238248
return (
239249
<div class="editor">
240250
<style dangerouslySetInnerHTML={styles} />
241-
<div class="canvasContainer" ref={canvasContainerRef}>
242-
<div class="cropButtonContainer" style={{ position: 'absolute' }} ref={cropContainerRef}>
251+
<div class="editor__canvas-container" ref={canvasContainerRef}>
252+
<div class="editor__crop-container" style={{ position: 'absolute' }} ref={cropContainerRef}>
243253
<canvas style={{ position: 'absolute' }} ref={croppingRef}></canvas>
244254
<CropCorner
245-
left={croppingRect.startX}
246-
top={croppingRect.startY}
255+
left={croppingRect.startX - CROP_BUTTON_BORDER}
256+
top={croppingRect.startY - CROP_BUTTON_BORDER}
247257
onGrabButton={onGrabButton}
248-
corner="topleft"
258+
corner="top-left"
249259
></CropCorner>
250260
<CropCorner
251-
left={croppingRect.endX - CROP_BUTTON_SIZE}
252-
top={croppingRect.startY}
261+
left={croppingRect.endX - CROP_BUTTON_SIZE + CROP_BUTTON_BORDER}
262+
top={croppingRect.startY - CROP_BUTTON_BORDER}
253263
onGrabButton={onGrabButton}
254-
corner="topright"
264+
corner="top-right"
255265
></CropCorner>
256266
<CropCorner
257-
left={croppingRect.startX}
258-
top={croppingRect.endY - CROP_BUTTON_SIZE}
267+
left={croppingRect.startX - CROP_BUTTON_BORDER}
268+
top={croppingRect.endY - CROP_BUTTON_SIZE + CROP_BUTTON_BORDER}
259269
onGrabButton={onGrabButton}
260-
corner="bottomleft"
270+
corner="bottom-left"
261271
></CropCorner>
262272
<CropCorner
263-
left={croppingRect.endX - CROP_BUTTON_SIZE}
264-
top={croppingRect.endY - CROP_BUTTON_SIZE}
273+
left={croppingRect.endX - CROP_BUTTON_SIZE + CROP_BUTTON_BORDER}
274+
top={croppingRect.endY - CROP_BUTTON_SIZE + CROP_BUTTON_BORDER}
265275
onGrabButton={onGrabButton}
266-
corner="bottomright"
276+
corner="bottom-right"
267277
></CropCorner>
268278
<div
269279
style={{
270280
left: Math.max(0, croppingRect.endX - 191),
271281
top: Math.max(0, croppingRect.endY + 8),
272282
display: confirmCrop ? 'flex' : 'none',
273283
}}
274-
class="crop-btn-group"
284+
class="editor__crop-btn-group"
275285
>
276286
<button
277287
onClick={e => {
@@ -280,8 +290,8 @@ export function makeScreenshotEditorComponent({ h, imageBuffer, dialog }: Factor
280290
setCroppingRect({
281291
startX: 0,
282292
startY: 0,
283-
endX: croppingRef.current.width,
284-
endY: croppingRef.current.height,
293+
endX: croppingRef.current.width / DPI,
294+
endY: croppingRef.current.height / DPI,
285295
});
286296
}
287297
setConfirmCrop(false);
@@ -321,16 +331,10 @@ function CropCorner({
321331
}): VNode {
322332
return (
323333
<button
324-
class="crop-btn"
334+
class={`editor__crop-corner editor__crop-corner--${corner} `}
325335
style={{
326336
top: top,
327337
left: left,
328-
borderTop: corner === 'topleft' || corner === 'topright' ? 'solid purple' : 'none',
329-
borderLeft: corner === 'topleft' || corner === 'bottomleft' ? 'solid purple' : 'none',
330-
borderRight: corner === 'topright' || corner === 'bottomright' ? 'solid purple' : 'none',
331-
borderBottom: corner === 'bottomleft' || corner === 'bottomright' ? 'solid purple' : 'none',
332-
borderWidth: `${CROP_BUTTON_BORDER}px`,
333-
cursor: corner === 'topleft' || corner === 'bottomright' ? 'nwse-resize' : 'nesw-resize',
334338
}}
335339
onMouseDown={e => {
336340
e.preventDefault();

packages/feedback/src/screenshot/components/ScreenshotInput.css.ts

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import { DOCUMENT } from '../../constants';
1+
import { CROP_COLOR, DOCUMENT } from '../../constants';
22

33
/**
44
* Creates <style> element for widget dialog
55
*/
66
export function createScreenshotInputStyles(): HTMLStyleElement {
77
const style = DOCUMENT.createElement('style');
88

9-
const surface200 = '#FAF9FB';
10-
const gray100 = '#F0ECF3';
9+
const surface200 = '#1A141F';
10+
const gray100 = '#302735';
1111

1212
style.textContent = `
1313
.dialog__content:has(.editor) {
@@ -16,6 +16,9 @@ export function createScreenshotInputStyles(): HTMLStyleElement {
1616
}
1717
1818
.editor {
19+
padding: 10px;
20+
padding-top: 65px;
21+
padding-bottom: 65px;
1922
flex-grow: 1;
2023
2124
background-color: ${surface200};
@@ -35,24 +38,19 @@ export function createScreenshotInputStyles(): HTMLStyleElement {
3538
);
3639
}
3740
38-
.canvasContainer {
41+
.editor__canvas-container {
3942
width: 100%;
4043
height: 100%;
4144
position: relative;
4245
}
4346
44-
.canvasContainer canvas {
47+
.editor__canvas-container canvas {
4548
width: 100%;
4649
height: 100%;
4750
object-fit: contain;
4851
}
4952
50-
.cropper {
51-
width: 100%;
52-
height: 100%;
53-
}
54-
55-
.crop-btn-group {
53+
.editor__crop-btn-group {
5654
padding: 8px;
5755
gap: 8px;
5856
border-radius: var(--form-content-border-radius);
@@ -61,11 +59,33 @@ export function createScreenshotInputStyles(): HTMLStyleElement {
6159
position: absolute;
6260
}
6361
64-
.crop-btn {
62+
.editor__crop-corner {
6563
width: 30px;
6664
height: 30px;
6765
position: absolute;
6866
background: none;
67+
border: 3px solid ${CROP_COLOR};
68+
}
69+
70+
.editor__crop-corner--top-left {
71+
cursor: nwse-resize;
72+
border-right: none;
73+
border-bottom: none;
74+
}
75+
.editor__crop-corner--top-right {
76+
cursor: nesw-resize;
77+
border-left: none;
78+
border-bottom: none;
79+
}
80+
.editor__crop-corner--bottom-left {
81+
cursor: nesw-resize;
82+
border-right: none;
83+
border-top: none;
84+
}
85+
.editor__crop-corner--bottom-right {
86+
cursor: nwse-resize;
87+
border-left: none;
88+
border-top: none;
6989
}
7090
`;
7191

0 commit comments

Comments
 (0)