Skip to content

Commit 9a0bdcd

Browse files
authored
[Image] | (CX) | Add a button to reset image to original dimensions (#3007)
## Summary: It could be helpful to provide a way to reset an image to its natural dimensions within the image widget editor. Issue: https://khanacademy.atlassian.net/browse/LEMS-3638 ## Test plan: `pnpm jest packages/perseus-editor/src/widgets/__tests__/image-editor.test.tsx` Storybook `/?path=/story/widgets-image-editor-demo--populated` https://github.com/user-attachments/assets/33ce9be2-fea6-476e-8147-b6f35452881e Author: nishasy Reviewers: ivyolamit Required Reviewers: Approved By: ivyolamit Checks: ✅ 10 checks were successful, ⏹️ 7 checks were cancelled Pull Request URL: #3007
1 parent 913d2ef commit 9a0bdcd

File tree

5 files changed

+127
-26
lines changed

5 files changed

+127
-26
lines changed

.changeset/lemon-seas-allow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@khanacademy/perseus-editor": patch
3+
---
4+
5+
[Image] | (CX) | Add a button to reset image to original dimensions

packages/perseus-editor/src/widgets/__tests__/image-editor.test.tsx

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ describe("image editor", () => {
5050
testDependencies,
5151
);
5252

53-
unmockImageLoading = mockImageLoading();
53+
unmockImageLoading = mockImageLoading({
54+
naturalWidth: earthMoonImage.width,
55+
naturalHeight: earthMoonImage.height,
56+
});
5457
});
5558

5659
afterEach(() => {
@@ -427,6 +430,54 @@ describe("image editor", () => {
427430
});
428431
});
429432

433+
it("should call onChange with original image size when reset to original size is clicked", async () => {
434+
// Arrange
435+
const onChangeMock = jest.fn();
436+
render(
437+
<ImageEditorWithDependencies
438+
apiOptions={apiOptions}
439+
backgroundImage={{
440+
url: earthMoonImage.url,
441+
width: earthMoonImage.width / 2,
442+
height: earthMoonImage.height / 2,
443+
}}
444+
onChange={onChangeMock}
445+
/>,
446+
);
447+
448+
// Act
449+
const resetToOriginalSizeButton = screen.getByRole("button", {
450+
name: "Reset to original size",
451+
});
452+
await userEvent.click(resetToOriginalSizeButton);
453+
454+
// Assert
455+
expect(onChangeMock).toHaveBeenCalledWith({
456+
backgroundImage: earthMoonImage,
457+
});
458+
});
459+
460+
it("should not call onChange when reset to original size is clicked and the image size is already the original size", async () => {
461+
// Arrange
462+
const onChangeMock = jest.fn();
463+
render(
464+
<ImageEditorWithDependencies
465+
apiOptions={apiOptions}
466+
backgroundImage={earthMoonImage}
467+
onChange={onChangeMock}
468+
/>,
469+
);
470+
471+
// Act
472+
const resetToOriginalSizeButton = screen.getByRole("button", {
473+
name: "Reset to original size",
474+
});
475+
await userEvent.click(resetToOriginalSizeButton);
476+
477+
// Assert
478+
expect(onChangeMock).not.toHaveBeenCalled();
479+
});
480+
430481
it("should call onChange with new alt text", async () => {
431482
// Arrange
432483
const onChangeMock = jest.fn();

packages/perseus-editor/src/widgets/image-editor/components/image-dimensions-input.tsx

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import {Util} from "@khanacademy/perseus";
2+
import Button from "@khanacademy/wonder-blocks-button";
13
import {LabeledField} from "@khanacademy/wonder-blocks-labeled-field";
4+
import arrowCounterClockwise from "@phosphor-icons/core/bold/arrow-counter-clockwise-bold.svg";
25
import * as React from "react";
36

47
import ScrolllessNumberTextField from "../../../components/scrollless-number-text-field";
@@ -60,29 +63,59 @@ export default function ImageDimensionsInput({
6063
});
6164
}
6265

66+
async function handleResetToOriginalSize() {
67+
const naturalSize = await Util.getImageSizeModern(backgroundImage.url!);
68+
const [naturalWidth, naturalHeight] = naturalSize;
69+
70+
if (
71+
naturalWidth === backgroundImage.width &&
72+
naturalHeight === backgroundImage.height
73+
) {
74+
return;
75+
}
76+
77+
onChange({
78+
backgroundImage: {
79+
...backgroundImage,
80+
width: naturalWidth,
81+
height: naturalHeight,
82+
},
83+
});
84+
}
85+
6386
return (
6487
<div className={styles.dimensionsContainer}>
65-
<LabeledField
66-
label="Width"
67-
field={
68-
<ScrolllessNumberTextField
69-
value={backgroundImage.width?.toString() ?? ""}
70-
onChange={handleWidthChange}
71-
/>
72-
}
73-
styles={wbFieldStyles}
74-
/>
75-
<span className={styles.xSpan}>x</span>
76-
<LabeledField
77-
label="Height"
78-
field={
79-
<ScrolllessNumberTextField
80-
value={backgroundImage.height?.toString() ?? ""}
81-
onChange={handleHeightChange}
82-
/>
83-
}
84-
styles={wbFieldStyles}
85-
/>
88+
<div className={styles.dimensionsFieldContainer}>
89+
<LabeledField
90+
label="Width"
91+
field={
92+
<ScrolllessNumberTextField
93+
value={backgroundImage.width?.toString() ?? ""}
94+
onChange={handleWidthChange}
95+
/>
96+
}
97+
styles={wbFieldStyles}
98+
/>
99+
<span className={styles.xSpan}>x</span>
100+
<LabeledField
101+
label="Height"
102+
field={
103+
<ScrolllessNumberTextField
104+
value={backgroundImage.height?.toString() ?? ""}
105+
onChange={handleHeightChange}
106+
/>
107+
}
108+
styles={wbFieldStyles}
109+
/>
110+
</div>
111+
<Button
112+
kind="tertiary"
113+
size="small"
114+
startIcon={arrowCounterClockwise}
115+
onClick={handleResetToOriginalSize}
116+
>
117+
Reset to original size
118+
</Button>
86119
</div>
87120
);
88121
}

packages/perseus-editor/src/widgets/image-editor/image-editor.module.css

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
.dimensions-container {
22
display: flex;
3-
flex-direction: row;
4-
align-items: center;
3+
flex-direction: column;
4+
align-items: start;
55
background-color: var(
66
--wb-semanticColor-core-background-instructive-subtle
77
);
@@ -12,6 +12,12 @@
1212
padding: var(--wb-sizing-size_080);
1313
}
1414

15+
.dimensions-field-container {
16+
display: flex;
17+
flex-direction: row;
18+
align-items: center;
19+
}
20+
1521
.x-span {
1622
display: block;
1723
margin-inline: var(--wb-sizing-size_080);

testing/image-loader-utils.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@ import {act} from "@testing-library/react";
55
* the load event.
66
* @returns A function to unmock the image loading
77
*/
8-
export const mockImageLoading = () => {
8+
export const mockImageLoading = (options?: {
9+
naturalWidth?: number;
10+
naturalHeight?: number;
11+
}) => {
912
const originalImage = window.Image;
1013

1114
const mockImage = jest.fn(() => {
12-
const img = {} as HTMLImageElement;
15+
const img = {
16+
naturalWidth: options?.naturalWidth,
17+
naturalHeight: options?.naturalHeight,
18+
} as HTMLImageElement;
1319
// Immediately trigger onload using setTimeout to ensure it happens after render
1420
setTimeout(() => {
1521
if (img.onload) {

0 commit comments

Comments
 (0)