Skip to content

Commit d60a719

Browse files
authored
[Image] | (CX) (a11y) | Add long description field to Image widget editor (#2908)
## Summary: Now that the `longDescription` field has been added to the Image widget, we can add the text field for it within the Image widget editor. Making sure to add it behind a feature flag. Issue: https://khanacademy.atlassian.net/browse/LEMS-3378 ## Test plan: `pnpm jest packages/perseus-editor/src/widgets/__tests__/image-editor.test.tsx` Storybook `/?path=/docs/widgets-image-editor-demo--docs#populated-within-editor-page` ## Screenshots and demos: <img width="368" height="633" alt="Screenshot 2025-09-16 at 4 24 29 PM" src="https://github.com/user-attachments/assets/4aa28574-5d62-41a3-aa0f-a1f7bf6714f8" /> https://github.com/user-attachments/assets/2c276193-ca29-491e-a956-55eea5895ff3 Author: nishasy Reviewers: nishasy, ivyolamit, catandthemachines Required Reviewers: Approved By: ivyolamit Checks: ✅ 10 checks were successful Pull Request URL: #2908
1 parent 902f636 commit d60a719

File tree

5 files changed

+117
-2
lines changed

5 files changed

+117
-2
lines changed

.changeset/lazy-pugs-teach.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@khanacademy/perseus": minor
3+
"@khanacademy/perseus-editor": minor
4+
---
5+
6+
[Image] | (CX) (a11y) | Add long description field to Image widget editor

packages/perseus-editor/src/widgets/__docs__/image-editor.stories.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import {ApiOptions} from "@khanacademy/perseus";
12
import {
23
generateImageOptions,
34
generateImageWidget,
45
generateTestPerseusRenderer,
56
} from "@khanacademy/perseus-core";
67
import * as React from "react";
78

9+
import {getFeatureFlags} from "../../../../../testing/feature-flags-util";
810
import EditorPageWithStorybookPreview from "../../__docs__/editor-page-with-storybook-preview";
911
import {registerAllWidgetsAndEditorsForTesting} from "../../util/register-all-widgets-and-editors-for-testing";
1012
import ImageEditor from "../image-editor/image-editor";
@@ -17,6 +19,12 @@ const withinEditorPageDecorator = (_, {args}) => {
1719
return (
1820
<div style={{width: PROD_EDITOR_WIDTH}}>
1921
<EditorPageWithStorybookPreview
22+
apiOptions={{
23+
...ApiOptions.defaults,
24+
flags: getFeatureFlags({
25+
"image-widget-upgrade": true,
26+
}),
27+
}}
2028
question={generateTestPerseusRenderer({
2129
content: "[[☃ image 1]]",
2230
widgets: {

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

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {render, screen} from "@testing-library/react";
33
import {userEvent as userEventLib} from "@testing-library/user-event";
44
import * as React from "react";
55

6+
import {getFeatureFlags} from "../../../../../testing/feature-flags-util";
67
import {testDependencies} from "../../../../../testing/test-dependencies";
78
import ImageEditor from "../image-editor/image-editor";
89

@@ -13,7 +14,10 @@ const realKhanImageUrl =
1314
const nonKhanImageWarning =
1415
"Images must be from sites hosted by Khan Academy. Please input a Khan Academy-owned address, or use the Add Image tool to rehost an existing image";
1516

16-
const apiOptions = ApiOptions.defaults;
17+
const apiOptions = {
18+
...ApiOptions.defaults,
19+
flags: getFeatureFlags({"image-widget-upgrade": true}),
20+
};
1721

1822
describe("image editor", () => {
1923
let userEvent: UserEvent;
@@ -43,6 +47,7 @@ describe("image editor", () => {
4347
expect(screen.queryByText("Preview:")).not.toBeInTheDocument();
4448
expect(screen.queryByText("Dimensions:")).not.toBeInTheDocument();
4549
expect(screen.queryByText("Alt text:")).not.toBeInTheDocument();
50+
expect(screen.queryByText("Long Description:")).not.toBeInTheDocument();
4651
expect(screen.queryByText("Caption:")).not.toBeInTheDocument();
4752
});
4853

@@ -55,6 +60,7 @@ describe("image editor", () => {
5560
apiOptions={apiOptions}
5661
backgroundImage={{url: realKhanImageUrl}}
5762
alt="Earth and moon alt"
63+
longDescription="Earth and moon long description"
5864
caption="Earth and moon caption"
5965
title="Earth and moon title"
6066
onChange={() => {}}
@@ -64,19 +70,26 @@ describe("image editor", () => {
6470
const dimensionsLabel = screen.getByText("Dimensions:");
6571
const urlField = screen.getByRole("textbox", {name: "Image url:"});
6672
const altField = screen.getByRole("textbox", {name: "Alt text:"});
73+
const longDescriptionField = screen.getByRole("textbox", {
74+
name: "Long Description:",
75+
});
6776
const captionField = screen.getByRole("textbox", {name: "Caption:"});
6877
const titleField = screen.getByRole("textbox", {name: "Title:"});
6978

7079
// Assert
7180
expect(dimensionsLabel).toBeInTheDocument();
7281
expect(urlField).toBeInTheDocument();
7382
expect(altField).toBeInTheDocument();
83+
expect(longDescriptionField).toBeInTheDocument();
7484
expect(captionField).toBeInTheDocument();
7585
expect(titleField).toBeInTheDocument();
7686

7787
expect(screen.getByText("unknown")).toBeInTheDocument();
7888
expect(urlField).toHaveValue(realKhanImageUrl);
7989
expect(altField).toHaveValue("Earth and moon alt");
90+
expect(longDescriptionField).toHaveValue(
91+
"Earth and moon long description",
92+
);
8093
expect(captionField).toHaveValue("Earth and moon caption");
8194
expect(titleField).toHaveValue("Earth and moon title");
8295
});
@@ -258,6 +271,71 @@ describe("image editor", () => {
258271
});
259272
});
260273

274+
it("should call onChange with new long description", async () => {
275+
// Arrange
276+
const onChangeMock = jest.fn();
277+
render(
278+
<ImageEditor
279+
apiOptions={apiOptions}
280+
backgroundImage={{url: realKhanImageUrl}}
281+
onChange={onChangeMock}
282+
/>,
283+
);
284+
285+
// Act
286+
const altField = screen.getByRole("textbox", {
287+
name: "Long Description:",
288+
});
289+
altField.focus();
290+
await userEvent.paste("Earth and moon long description");
291+
292+
// Assert
293+
expect(onChangeMock).toHaveBeenCalledWith({
294+
longDescription: "Earth and moon long description",
295+
});
296+
});
297+
298+
it("should call onChange with empty long description", async () => {
299+
// Arrange
300+
const onChangeMock = jest.fn();
301+
render(
302+
<ImageEditor
303+
apiOptions={apiOptions}
304+
backgroundImage={{url: realKhanImageUrl}}
305+
longDescription="Earth and moon long description"
306+
onChange={onChangeMock}
307+
/>,
308+
);
309+
310+
// Act
311+
const altField = screen.getByRole("textbox", {
312+
name: "Long Description:",
313+
});
314+
await userEvent.clear(altField);
315+
316+
// Assert
317+
expect(onChangeMock).toHaveBeenCalledWith({
318+
longDescription: "",
319+
});
320+
});
321+
322+
it("should not render long description field if the feature flag is off", () => {
323+
// Arrange
324+
const onChangeMock = jest.fn();
325+
render(
326+
<ImageEditor
327+
apiOptions={{
328+
...ApiOptions.defaults,
329+
flags: getFeatureFlags({"image-widget-upgrade": false}),
330+
}}
331+
onChange={onChangeMock}
332+
/>,
333+
);
334+
335+
// Assert
336+
expect(screen.queryByText("Long Description:")).not.toBeInTheDocument();
337+
});
338+
261339
it("should call onChange with new caption", async () => {
262340
// Arrange
263341
const onChangeMock = jest.fn();

packages/perseus-editor/src/widgets/image-editor/image-settings.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {Util, components} from "@khanacademy/perseus";
2+
import {isFeatureOn} from "@khanacademy/perseus-core";
23
import {sizing} from "@khanacademy/wonder-blocks-tokens";
34
import {HeadingXSmall} from "@khanacademy/wonder-blocks-typography";
45
import * as React from "react";
@@ -14,14 +15,19 @@ const {InfoTip} = components;
1415
export default function ImageSettings({
1516
alt,
1617
backgroundImage,
18+
apiOptions,
1719
caption,
20+
longDescription,
1821
title,
1922
onChange,
2023
}: Props) {
2124
const uniqueId = React.useId();
2225
const altId = `${uniqueId}-alt`;
2326
const titleId = `${uniqueId}-title`;
2427
const captionId = `${uniqueId}-caption`;
28+
const longDescriptionId = `${uniqueId}-long-description`;
29+
30+
const imageUpgradeFF = isFeatureOn({apiOptions}, "image-widget-upgrade");
2531

2632
if (!backgroundImage.url) {
2733
return null;
@@ -93,6 +99,21 @@ export default function ImageSettings({
9399
style={textAreaStyle}
94100
/>
95101

102+
{imageUpgradeFF && (
103+
<>
104+
{/* Long Description */}
105+
<HeadingXSmall tag="label" htmlFor={longDescriptionId}>
106+
Long Description:
107+
</HeadingXSmall>
108+
<AutoResizingTextArea
109+
id={longDescriptionId}
110+
value={longDescription ?? ""}
111+
onChange={(value) => onChange({longDescription: value})}
112+
style={textAreaStyle}
113+
/>
114+
</>
115+
)}
116+
96117
{/* Title */}
97118
<HeadingXSmall tag="label" htmlFor={titleId}>
98119
Title:

packages/perseus/src/widgets/image/image.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {isFeatureOn} from "@khanacademy/perseus-core";
12
import * as React from "react";
23

34
import AssetContext from "../../asset-context";
@@ -25,6 +26,7 @@ export const ImageComponent = (props: ImageWidgetProps) => {
2526
trackInteraction,
2627
} = props;
2728
const context = React.useContext(PerseusI18nContext);
29+
const imageUpgradeFF = isFeatureOn({apiOptions}, "image-widget-upgrade");
2830

2931
if (!backgroundImage.url) {
3032
return null;
@@ -76,7 +78,7 @@ export const ImageComponent = (props: ImageWidgetProps) => {
7678
</AssetContext.Consumer>
7779

7880
{/* Description & Caption */}
79-
{(caption || longDescription) && (
81+
{(caption || (imageUpgradeFF && longDescription)) && (
8082
<ImageDescriptionAndCaption {...props} />
8183
)}
8284
</figure>

0 commit comments

Comments
 (0)