Skip to content

Commit 1e4362e

Browse files
authored
feat(user feedback): Adds toolbar for cropping and annotating (#15282)
- adds a toolbar for cropping and annotations - changes from inline styles to multiple class names in BEM format With annotation option: ![Screenshot 2025-02-03 at 3 51 04 PM](https://github.com/user-attachments/assets/97e4ac38-4926-49e5-a6f3-d474174e3c38) Without annotation option (to confirm that it looks the same as before): ![Screenshot 2025-02-03 at 5 09 01 PM](https://github.com/user-attachments/assets/8b614c38-3c1b-4d7e-986e-ead86a3f4349) Closes #15252
1 parent a3e08ad commit 1e4362e

File tree

4 files changed

+166
-87
lines changed

4 files changed

+166
-87
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { VNode, h as hType } from 'preact';
2+
3+
interface FactoryParams {
4+
h: typeof hType;
5+
}
6+
7+
export default function CropIconFactory({
8+
h, // eslint-disable-line @typescript-eslint/no-unused-vars
9+
}: FactoryParams) {
10+
return function CropIcon(): VNode {
11+
return (
12+
<svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
13+
<path
14+
d="M15.25 12.5H12.5M12.5 12.5H4.50001C3.94773 12.5 3.50001 12.0523 3.50001 11.5V3.50002M12.5 12.5L12.5 4.50002C12.5 3.94773 12.0523 3.50002 11.5 3.50002H3.50001M12.5 12.5L12.5 15.25M3.50001 3.50002V0.750031M3.50001 3.50002H0.75"
15+
stroke="currentColor"
16+
strokeWidth="1.5"
17+
strokeLinecap="round"
18+
strokeLinejoin="round"
19+
/>
20+
</svg>
21+
);
22+
};
23+
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export default function PenIconFactory({
99
}: FactoryParams) {
1010
return function PenIcon(): VNode {
1111
return (
12-
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
12+
<svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
1313
<path
1414
d="M8.5 12L12 8.5L14 11L11 14L8.5 12Z"
1515
stroke="currentColor"

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

+100-80
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { h } from 'preact'; // eslint-disable-line @typescript-eslint/no-unused-
66
import type * as Hooks from 'preact/hooks';
77
import { DOCUMENT, WINDOW } from '../../constants';
88
import CropCornerFactory from './CropCorner';
9+
import CropIconFactory from './CropIcon';
910
import PenIconFactory from './PenIcon';
1011
import { createScreenshotInputStyles } from './ScreenshotInput.css';
1112
import { useTakeScreenshotFactory } from './useTakeScreenshot';
@@ -75,6 +76,7 @@ export function ScreenshotEditorFactory({
7576
const useTakeScreenshot = useTakeScreenshotFactory({ hooks });
7677
const CropCorner = CropCornerFactory({ h });
7778
const PenIcon = PenIconFactory({ h });
79+
const CropIcon = CropIconFactory({ h });
7880

7981
return function ScreenshotEditor({ onError }: Props): VNode {
8082
const styles = hooks.useMemo(() => ({ __html: createScreenshotInputStyles(options.styleNonce).innerText }), []);
@@ -86,6 +88,7 @@ export function ScreenshotEditorFactory({
8688
const [croppingRect, setCroppingRect] = hooks.useState<Box>({ startX: 0, startY: 0, endX: 0, endY: 0 });
8789
const [confirmCrop, setConfirmCrop] = hooks.useState(false);
8890
const [isResizing, setIsResizing] = hooks.useState(false);
91+
const [isCropping, setIsCropping] = hooks.useState(true);
8992
const [isAnnotating, setIsAnnotating] = hooks.useState(false);
9093

9194
hooks.useEffect(() => {
@@ -142,6 +145,10 @@ export function ScreenshotEditorFactory({
142145
const croppingBox = constructRect(croppingRect);
143146
ctx.clearRect(0, 0, imageDimensions.width, imageDimensions.height);
144147

148+
if (!isCropping) {
149+
return;
150+
}
151+
145152
// draw gray overlay around the selection
146153
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
147154
ctx.fillRect(0, 0, imageDimensions.width, imageDimensions.height);
@@ -154,7 +161,7 @@ export function ScreenshotEditorFactory({
154161
ctx.strokeStyle = '#000000';
155162
ctx.lineWidth = 1;
156163
ctx.strokeRect(croppingBox.x + 3, croppingBox.y + 3, croppingBox.width - 6, croppingBox.height - 6);
157-
}, [croppingRect]);
164+
}, [croppingRect, isCropping]);
158165

159166
function onGrabButton(e: Event, corner: string): void {
160167
setIsAnnotating(false);
@@ -398,102 +405,115 @@ export function ScreenshotEditorFactory({
398405
return (
399406
<div class="editor">
400407
<style nonce={options.styleNonce} dangerouslySetInnerHTML={styles} />
401-
{options._experiments.annotations && (
402-
<div class="editor__tool-container">
403-
<button
404-
class="editor__pen-tool"
405-
style={{
406-
background: isAnnotating
407-
? 'var(--button-primary-background, var(--accent-background))'
408-
: 'var(--button-background, var(--background))',
409-
color: isAnnotating
410-
? 'var(--button-primary-foreground, var(--accent-foreground))'
411-
: 'var(--button-foreground, var(--foreground))',
412-
}}
413-
onClick={e => {
414-
e.preventDefault();
415-
setIsAnnotating(!isAnnotating);
416-
}}
408+
<div class="editor__image-container">
409+
<div class="editor__canvas-container" ref={canvasContainerRef}>
410+
<div
411+
class={`editor__crop-container ${isAnnotating ? 'editor__crop-container--inactive' : ''}
412+
${confirmCrop ? 'editor__crop-container--move' : ''}`}
413+
ref={cropContainerRef}
417414
>
418-
<PenIcon />
419-
</button>
420-
</div>
421-
)}
422-
<div class="editor__canvas-container" ref={canvasContainerRef}>
423-
<div class="editor__crop-container" style={{ zIndex: isAnnotating ? 1 : 2 }} ref={cropContainerRef}>
415+
<canvas onMouseDown={onDragStart} ref={croppingRef}></canvas>
416+
{isCropping && (
417+
<div>
418+
<CropCorner
419+
left={croppingRect.startX - CROP_BUTTON_BORDER}
420+
top={croppingRect.startY - CROP_BUTTON_BORDER}
421+
onGrabButton={onGrabButton}
422+
corner="top-left"
423+
></CropCorner>
424+
<CropCorner
425+
left={croppingRect.endX - CROP_BUTTON_SIZE + CROP_BUTTON_BORDER}
426+
top={croppingRect.startY - CROP_BUTTON_BORDER}
427+
onGrabButton={onGrabButton}
428+
corner="top-right"
429+
></CropCorner>
430+
<CropCorner
431+
left={croppingRect.startX - CROP_BUTTON_BORDER}
432+
top={croppingRect.endY - CROP_BUTTON_SIZE + CROP_BUTTON_BORDER}
433+
onGrabButton={onGrabButton}
434+
corner="bottom-left"
435+
></CropCorner>
436+
<CropCorner
437+
left={croppingRect.endX - CROP_BUTTON_SIZE + CROP_BUTTON_BORDER}
438+
top={croppingRect.endY - CROP_BUTTON_SIZE + CROP_BUTTON_BORDER}
439+
onGrabButton={onGrabButton}
440+
corner="bottom-right"
441+
></CropCorner>
442+
</div>
443+
)}
444+
{isCropping && (
445+
<div
446+
style={{
447+
left: Math.max(0, croppingRect.endX - 191),
448+
top: Math.max(0, croppingRect.endY + 8),
449+
}}
450+
class={`editor__crop-btn-group ${confirmCrop ? 'editor__crop-btn-group--active' : ''}`}
451+
>
452+
<button
453+
onClick={e => {
454+
e.preventDefault();
455+
if (croppingRef.current) {
456+
setCroppingRect({
457+
startX: 0,
458+
startY: 0,
459+
endX: croppingRef.current.width / DPI,
460+
endY: croppingRef.current.height / DPI,
461+
});
462+
}
463+
setConfirmCrop(false);
464+
}}
465+
class="btn btn--default"
466+
>
467+
{options.cancelButtonLabel}
468+
</button>
469+
<button
470+
onClick={e => {
471+
e.preventDefault();
472+
applyCrop();
473+
setConfirmCrop(false);
474+
}}
475+
class="btn btn--primary"
476+
>
477+
{options.confirmButtonLabel}
478+
</button>
479+
</div>
480+
)}
481+
</div>
424482
<canvas
425-
onMouseDown={onDragStart}
426-
style={{ cursor: confirmCrop ? 'move' : 'auto' }}
427-
ref={croppingRef}
483+
class={`editor__annotation ${isAnnotating ? 'editor__annotation--active' : ''}`}
484+
onMouseDown={onAnnotateStart}
485+
ref={annotatingRef}
428486
></canvas>
429-
<CropCorner
430-
left={croppingRect.startX - CROP_BUTTON_BORDER}
431-
top={croppingRect.startY - CROP_BUTTON_BORDER}
432-
onGrabButton={onGrabButton}
433-
corner="top-left"
434-
></CropCorner>
435-
<CropCorner
436-
left={croppingRect.endX - CROP_BUTTON_SIZE + CROP_BUTTON_BORDER}
437-
top={croppingRect.startY - CROP_BUTTON_BORDER}
438-
onGrabButton={onGrabButton}
439-
corner="top-right"
440-
></CropCorner>
441-
<CropCorner
442-
left={croppingRect.startX - CROP_BUTTON_BORDER}
443-
top={croppingRect.endY - CROP_BUTTON_SIZE + CROP_BUTTON_BORDER}
444-
onGrabButton={onGrabButton}
445-
corner="bottom-left"
446-
></CropCorner>
447-
<CropCorner
448-
left={croppingRect.endX - CROP_BUTTON_SIZE + CROP_BUTTON_BORDER}
449-
top={croppingRect.endY - CROP_BUTTON_SIZE + CROP_BUTTON_BORDER}
450-
onGrabButton={onGrabButton}
451-
corner="bottom-right"
452-
></CropCorner>
453-
<div
454-
style={{
455-
left: Math.max(0, croppingRect.endX - 191),
456-
top: Math.max(0, croppingRect.endY + 8),
457-
display: confirmCrop ? 'flex' : 'none',
458-
}}
459-
class="editor__crop-btn-group"
460-
>
487+
</div>
488+
</div>
489+
{options._experiments.annotations && (
490+
<div class="editor__tool-container">
491+
<div />
492+
<div class="editor__tool-bar">
461493
<button
494+
class={`editor__tool ${isCropping ? 'editor__tool--active' : ''}`}
462495
onClick={e => {
463496
e.preventDefault();
464-
if (croppingRef.current) {
465-
setCroppingRect({
466-
startX: 0,
467-
startY: 0,
468-
endX: croppingRef.current.width / DPI,
469-
endY: croppingRef.current.height / DPI,
470-
});
471-
}
472-
setConfirmCrop(false);
497+
setIsCropping(!isCropping);
498+
setIsAnnotating(false);
473499
}}
474-
class="btn btn--default"
475500
>
476-
{options.cancelButtonLabel}
501+
<CropIcon />
477502
</button>
478503
<button
504+
class={`editor__tool ${isAnnotating ? 'editor__tool--active' : ''}`}
479505
onClick={e => {
480506
e.preventDefault();
481-
applyCrop();
482-
setConfirmCrop(false);
507+
setIsAnnotating(!isAnnotating);
508+
setIsCropping(false);
483509
}}
484-
class="btn btn--primary"
485510
>
486-
{options.confirmButtonLabel}
511+
<PenIcon />
487512
</button>
488513
</div>
514+
<div />
489515
</div>
490-
<canvas
491-
class="editor__annotation"
492-
onMouseDown={onAnnotateStart}
493-
style={{ zIndex: isAnnotating ? '2' : '1' }}
494-
ref={annotatingRef}
495-
></canvas>
496-
</div>
516+
)}
497517
</div>
498518
);
499519
};

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

+42-6
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,17 @@ export function createScreenshotInputStyles(styleNonce?: string): HTMLStyleEleme
1111

1212
style.textContent = `
1313
.editor {
14+
display: flex;
15+
flex-grow: 1;
16+
flex-direction: column;
17+
}
18+
.editor__image-container {
1419
padding: 10px;
1520
padding-top: 65px;
1621
padding-bottom: 65px;
17-
flex-grow: 1;
1822
position: relative;
23+
height: 100%;
24+
border-radius: var(--menu-border-radius, 6px);
1925
2026
background-color: ${surface200};
2127
background-image: repeating-linear-gradient(
@@ -34,6 +40,13 @@ export function createScreenshotInputStyles(styleNonce?: string): HTMLStyleEleme
3440
);
3541
}
3642
43+
.editor__annotation {
44+
z-index: 1;
45+
}
46+
.editor__annotation--active {
47+
z-index: 2;
48+
}
49+
3750
.editor__canvas-container {
3851
width: 100%;
3952
height: 100%;
@@ -49,7 +62,15 @@ export function createScreenshotInputStyles(styleNonce?: string): HTMLStyleEleme
4962
}
5063
5164
.editor__crop-container {
65+
custor: auto;
5266
position: absolute;
67+
z-index: 2;
68+
}
69+
.editor__crop-container--inactive {
70+
z-index: 1;
71+
}
72+
.editor__crop-container--move {
73+
cursor: move;
5374
}
5475
5576
.editor__crop-btn-group {
@@ -59,6 +80,10 @@ export function createScreenshotInputStyles(styleNonce?: string): HTMLStyleEleme
5980
background: var(--button-background, var(--background));
6081
width: 175px;
6182
position: absolute;
83+
display: none;
84+
}
85+
.editor__crop-btn-group--active {
86+
display: flex;
6287
}
6388
6489
.editor__crop-corner {
@@ -90,17 +115,28 @@ export function createScreenshotInputStyles(styleNonce?: string): HTMLStyleEleme
90115
border-top: none;
91116
}
92117
.editor__tool-container {
93-
position: absolute;
94-
padding: 10px 0px;
95-
top: 0;
118+
padding-top: 8px;
119+
display: flex;
120+
justify-content: space-between;
96121
}
97-
.editor__pen-tool {
98-
height: 30px;
122+
.editor__tool-bar {
99123
display: flex;
124+
gap: 8px;
125+
}
126+
.editor__tool {
127+
display: flex;
128+
padding: 8px 12px;
100129
justify-content: center;
101130
align-items: center;
102131
border: var(--button-border, var(--border));
103132
border-radius: var(--button-border-radius, 6px);
133+
background: var(--button-background, var(--background));
134+
color: var(--button-foreground, var(--foreground));
135+
}
136+
137+
.editor__tool--active {
138+
background: var(--button-primary-background, var(--accent-background));
139+
color: var(--button-primary-foreground, var(--accent-foreground));
104140
}
105141
`;
106142

0 commit comments

Comments
 (0)