[Image] | (UX) | Show image in explore modal even when size is undefined#3367
[Image] | (UX) | Show image in explore modal even when size is undefined#3367
Conversation
…mal without feature flag
…image in explore modal even when size is undefined
🗄️ Schema Change: No Changes ✅ |
|
Size Change: +11 B (0%) Total Size: 486 kB
ℹ️ View Unchanged
|
🛠️ Item Splitting: No Changes ✅ |
npm Snapshot: PublishedGood news!! We've packaged up the latest commit from this PR (dfbab76) and published it to npm. You Example: pnpm add @khanacademy/perseus@PR3367If you are working in Khan Academy's frontend, you can run the below command. ./dev/tools/bump_perseus_version.ts -t PR3367If you are working in Khan Academy's webapp, you can run the below command. ./dev/tools/bump_perseus_version.js -t PR3367 |
| // bgWidth / bgHeight = X / modalImageHeight | ||
| // => X = (bgWidth / bgHeight) * modalImageHeight | ||
| width = | ||
| (backgroundImage.width / backgroundImage.height) * modalImageHeight; |
There was a problem hiding this comment.
I considered removing this block entirely, since the sizing just works with undefined now. However, I opted to keep this block here since it will be useful for the work in my other branch on scaled graphie labels.
There was a problem hiding this comment.
This file has a lot of changes because of the boilerplate code that had to be updated to allow the image to actually load for testing.
| !backgroundImage.height || | ||
| !backgroundImage.width || | ||
| !backgroundImage.url | ||
| ) { |
| background-color: var(--wb-semanticColor-core-background-base-default); | ||
| /* Stop large images from overflowing out of the image area of the modal. */ | ||
| max-width: 100%; | ||
| max-height: var(--modal-panel-height); |
There was a problem hiding this comment.
Large images would overflow and cover the long description without this CSS.
There was a problem hiding this comment.
This CSS looks appropriate. However, I would prefer some changes to how the CSS variables are handled. I realize that this may be beyond the intent of this PR, but these changes will help to avoid potential conflicts.
- Namespace the CSS variables
- Instead of
--modal-panel-height, I recommend using--perseus-image-modal-panel-height - This will prevent naming conflicts with other parts of the application that have a modal panel height variable of the same name.
- Instead of
- Attach the CSS variables to
.modal-containerinstead of:root- Even if variables are namespaced, adding them to the
:rootelement overloads that element with variables that don't apply anywhere else other than the image widget. - If the variables are NOT namespaced, then this runs the risk of overriding the variable if it exists anywhere else in frontend, or of being overridden by a setting elsewhere in frontend.
- Even if variables are namespaced, adding them to the
Claude Code ReviewThis repository is configured for manual code reviews. Comment |
|
Tested it in my local server with a PR tag using the exercise that I first noticed this issue in:
|
| display: flex; | ||
| width: 100%; | ||
| justify-content: center; | ||
| } |
There was a problem hiding this comment.
@mark-fitzgerald Is it okay for .modal-container to be defined in two spots like this to separate out where the variables are defined?
There was a problem hiding this comment.
Technically it is OK, but I find it confusing. Is there a reason that it is split up?
There was a problem hiding this comment.
It's just split up so the variables can be at the top, and the styles can be further down with the other modal styles. Maybe I should move the modal styles to their own file so that the modal container can be at the top with the variables AND the style?
There was a problem hiding this comment.
There's no requirement for the variables to be at the top of the file. It's not like JavaScript.
|
@claude review |
| let modalImageHeight: number | undefined = Math.min( | ||
| MODAL_HEIGHT, | ||
| zoomHeight, | ||
| ); | ||
| // bgWidth / bgHeight = X / modalImageHeight | ||
| // => X = (bgWidth / bgHeight) * modalImageHeight | ||
| let width = (zoomWidth / zoomHeight) * modalImageHeight; | ||
| let width: number | undefined = (zoomWidth / zoomHeight) * modalImageHeight; |
There was a problem hiding this comment.
🟡 When the scale feature flag is OFF (the default path) and the image has no saved dimensions, zoomSize is [0, 0], producing modalImageHeight = 0 and width = NaN (from 0/0 * 0) at lines 52-58. These values make the image invisible in the modal. The scaleFF path handles this correctly (lines 72-77 fall back to undefined), but the non-scaleFF path needs the same guard -- e.g. set width and modalImageHeight to undefined when zoomHeight is 0.
Extended reasoning...
What the bug is
In explore-image-modal-content.tsx, lines 52-58 compute the modal image dimensions for the non-scaleFF (legacy/default) path:
let modalImageHeight = Math.min(MODAL_HEIGHT, zoomHeight); // Math.min(568, 0) = 0
let width = (zoomWidth / zoomHeight) * modalImageHeight; // (0 / 0) * 0 = NaNWhen the image has no saved width/height, zoomSize is initialized to [0, 0] in image.tsx:42-44 (backgroundImage.width || 0, backgroundImage.height || 0). This means zoomHeight = 0 and zoomWidth = 0, yielding modalImageHeight = 0 and width = NaN.
Why this PR introduced it
Before this PR, there was an early return guard that checked !backgroundImage.height || !backgroundImage.width || !backgroundImage.url. If the image had no saved dimensions, the component returned null and never reached the dimension calculation. This PR correctly removed the height/width checks from the guard (to allow images without saved dimensions to display), and the scaleFF path was updated with a proper fallback (lines 72-77 set width = undefined and modalImageHeight = undefined when dimensions are missing). However, the non-scaleFF path at lines 52-58 was not given the same fallback treatment.
Step-by-step proof
- Content has an image with no saved
width/heightbut has aurlandlongDescription. image.tsx:42-44initializeszoomSizeto[0, 0].useEffectat line 68 firesgetImageSizeModern()asynchronously.- Before the async resolution completes, or if
getImageSizeModernrejects (note: there is no.catch()handler at line 75),zoomSizeremains[0, 0]. - If the user opens the Explore Image modal during this window,
ExploreImageModalContentreceiveszoomSize = [0, 0]. - Line 52:
modalImageHeight = Math.min(568, 0) = 0. - Line 58:
width = (0 / 0) * 0 = NaN. - These are passed to
SvgImageasheight={0}andwidth={NaN}, resulting in an invisible image.
Practical impact and addressing the refutation
One verifier refuted this bug, arguing the scenario cannot occur because getImageSizeModern resolves before the user can click. This is largely correct for the happy path: the image is typically cached from the initial display, so async resolution is near-instant. However, there are two edge cases: (1) if the image load is slow or fails, there is no .catch() on the promise at image.tsx:75, so zoomSize stays [0, 0] permanently; (2) it is a transient state that exists between initial render and async resolution. Additionally, the non-scaleFF path is legacy code marked for removal (TODO LEMS-3912), further reducing the practical impact.
How to fix
Add the same fallback used in the scaleFF path. Before the if (scaleFF) block, add a guard:
if (!zoomHeight || !zoomWidth) {
width = undefined;
modalImageHeight = undefined;
}This lets the CSS max-width/max-height rules (newly added in this PR) handle the sizing naturally, consistent with the scaleFF approach.
There was a problem hiding this comment.
In image.tsx, the zoomSize is determined in a useEffect on load and then passed down into ImageInfoArea => ExploreImageModal => ExploreImageModalContent. So it would never actually be 0 in practice.
There was a problem hiding this comment.
Plus I'd prefer to avoid changing the non-flag code for now. We haven't had issues in production code, so there's no need to update this yet.
| expect(pauseButton).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("should have null content if there is no background image URL with scale flag enabled", () => { |
There was a problem hiding this comment.
I'm not familiar with this state. Could you explain a little more on the case where we would expect this? This seems like a state we should ward against.
There was a problem hiding this comment.
I don't think it's actually possible to ever reach this case.
catandthemachines
left a comment
There was a problem hiding this comment.
Approved, thank you!
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @khanacademy/perseus-editor@30.0.0 ### Major Changes - [#3332](#3332) [`604b3a6c25`](604b3a6) Thanks [@benchristel](https://github.com/benchristel)! - The `options` parameter of the `serialize` method of `EditorPage` and `Editor` has been removed. - [#3386](#3386) [`7e76fbbc2f`](7e76fbb) Thanks [@benchristel](https://github.com/benchristel)! - The `serialize` methods of classes in `@khanacademy/perseus-editor` no longer use arrow function syntax. Callers should not unbind them from the class instance. Additionally, the `Editor` component no longer accepts a `replace` prop (used for hints), and its serialize method no longer returns `replace`. The `replace` prop was only used in `serialize`. Users of the `Editor` component should manage hints' `replace` setting themselves. ### Minor Changes - [#3395](#3395) [`97223334ea`](9722333) Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! - Implementation of Editor support for Exponential Graph - [#3352](#3352) [`b681e00a4f`](b681e00) Thanks [@handeyeco](https://github.com/handeyeco)! - Add editor support for AbsoluteValue - [#3348](#3348) [`b1557c2a73`](b1557c2) Thanks [@handeyeco](https://github.com/handeyeco)! - Add schema for AbsoluteValue graph - [#3345](#3345) [`dde985f3b5`](dde985f) Thanks [@ivyolamit](https://github.com/ivyolamit)! - Add tangent type definitions, this is the initial implementation for supporting Tangent graph in Interactive Graph - [#3358](#3358) [`8c503171b1`](8c50317) Thanks [@ivyolamit](https://github.com/ivyolamit)! - Add tangent graph option in the Interactive Graph Editor - [#3376](#3376) [`8aa0a77886`](8aa0a77) Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! - Creation of new Types, Schema, and Kmath utilities for Exponential Graph ### Patch Changes - [#3396](#3396) [`35fa9133db`](35fa913) Thanks [@nishasy](https://github.com/nishasy)! - [Image] | (CX) | Add a linter warning for images with no size - [#3390](#3390) [`d22c50dc2a`](d22c50d) Thanks [@nishasy](https://github.com/nishasy)! - [Image] | (CX) | Make the 125 character alt text warning less aggressive - [#3372](#3372) [`3cdb09813d`](3cdb098) Thanks [@nishasy](https://github.com/nishasy)! - [Image] | (UX) | Upscale Graphies within Explore Image Modal - [#3391](#3391) [`2f285ee161`](2f285ee) Thanks [@nishasy](https://github.com/nishasy)! - [Image] | (CX) | Add character counter to alt text field - [#3374](#3374) [`cd73c99ba3`](cd73c99) Thanks [@ivyolamit](https://github.com/ivyolamit)! - Remove incorrect usage of the feature flag setting in one of the test - Updated dependencies \[[`f18c0d9b6f`](f18c0d9), [`a022e751d6`](a022e75), [`35fa9133db`](35fa913), [`54db3fd4bd`](54db3fd), [`97223334ea`](9722333), [`027a5edbda`](027a5ed), [`ae0538d0a7`](ae0538d), [`005e13d784`](005e13d), [`3cdb09813d`](3cdb098), [`afcff9f96f`](afcff9f), [`75f184e5a7`](75f184e), [`4b2a7c85db`](4b2a7c8), [`5e1acd01f8`](5e1acd0), [`b681e00a4f`](b681e00), [`d99f1c0259`](d99f1c0), [`54eee35d65`](54eee35), [`b1557c2a73`](b1557c2), [`dde985f3b5`](dde985f), [`56e7dbe9a2`](56e7dbe), [`85f9cd46fc`](85f9cd4), [`8c503171b1`](8c50317), [`3aca3dcdf4`](3aca3dc), [`9f29bc7161`](9f29bc7), [`7034844845`](7034844), [`8aa0a77886`](8aa0a77), [`003aca7612`](003aca7)]: - @khanacademy/perseus-linter@4.9.0 - @khanacademy/perseus-score@8.4.0 - @khanacademy/perseus-core@23.7.0 - @khanacademy/perseus@76.1.0 - @khanacademy/kmath@2.3.0 - @khanacademy/keypad-context@3.2.40 - @khanacademy/math-input@26.4.10 ## @khanacademy/kmath@2.3.0 ### Minor Changes - [#3351](#3351) [`005e13d784`](005e13d) Thanks [@handeyeco](https://github.com/handeyeco)! - Add scoring for AbsoluteValue - [#3347](#3347) [`d99f1c0259`](d99f1c0) Thanks [@ivyolamit](https://github.com/ivyolamit)! - Add the tangent math utilities to kmath for supporting Tangent graph in Interactive Graph - [#3376](#3376) [`8aa0a77886`](8aa0a77) Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! - Creation of new Types, Schema, and Kmath utilities for Exponential Graph ### Patch Changes - Updated dependencies \[[`54db3fd4bd`](54db3fd), [`ae0538d0a7`](ae0538d), [`005e13d784`](005e13d), [`b1557c2a73`](b1557c2), [`dde985f3b5`](dde985f), [`8aa0a77886`](8aa0a77)]: - @khanacademy/perseus-core@23.7.0 ## @khanacademy/perseus@76.1.0 ### Minor Changes - [#3350](#3350) [`75f184e5a7`](75f184e) Thanks [@handeyeco](https://github.com/handeyeco)! - Implement AbsoluteValue rendering - [#3354](#3354) [`4b2a7c85db`](4b2a7c8) Thanks [@ivyolamit](https://github.com/ivyolamit)! - Created the tangent graph visual component, add Storybook coverage, SR strings, and equation string for supporting Tangent graph in Interactive Graph - [#3353](#3353) [`5e1acd01f8`](5e1acd0) Thanks [@ivyolamit](https://github.com/ivyolamit)! - Add tangent graph state management and reducer for supporting Tangent graph in Interactive Graph - [#3352](#3352) [`b681e00a4f`](b681e00) Thanks [@handeyeco](https://github.com/handeyeco)! - Add editor support for AbsoluteValue - [#3348](#3348) [`b1557c2a73`](b1557c2) Thanks [@handeyeco](https://github.com/handeyeco)! - Add schema for AbsoluteValue graph - [#3345](#3345) [`dde985f3b5`](dde985f) Thanks [@ivyolamit](https://github.com/ivyolamit)! - Add tangent type definitions, this is the initial implementation for supporting Tangent graph in Interactive Graph - [#3349](#3349) [`56e7dbe9a2`](56e7dbe) Thanks [@handeyeco](https://github.com/handeyeco)! - Add state management for AbsoluteValue - [#3377](#3377) [`85f9cd46fc`](85f9cd4) Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! - Implementation of state management logic for new Exponential graph - [#3358](#3358) [`8c503171b1`](8c50317) Thanks [@ivyolamit](https://github.com/ivyolamit)! - Add tangent graph option in the Interactive Graph Editor - [#3393](#3393) [`9f29bc7161`](9f29bc7) Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! - Rendering logic for new Exponential Graph - [#3376](#3376) [`8aa0a77886`](8aa0a77) Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! - Creation of new Types, Schema, and Kmath utilities for Exponential Graph ### Patch Changes - [#3329](#3329) [`027a5edbda`](027a5ed) Thanks [@Myranae](https://github.com/Myranae)! - Fix image bug by batching setState calls in setupGraphie - [#3372](#3372) [`3cdb09813d`](3cdb098) Thanks [@nishasy](https://github.com/nishasy)! - [Image] | (UX) | Upscale Graphies within Explore Image Modal - [#3365](#3365) [`afcff9f96f`](afcff9f) Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Improve ordering of Props type for `Renderer` component - [#3367](#3367) [`54eee35d65`](54eee35) Thanks [@nishasy](https://github.com/nishasy)! - [Image] | (UX) | Show image in explore modal even when size is undefined - [#3407](#3407) [`3aca3dcdf4`](3aca3dc) Thanks [@Myranae](https://github.com/Myranae)! - Improve a11y with graded group set - [#3385](#3385) [`003aca7612`](003aca7) Thanks [@Myranae](https://github.com/Myranae)! - Small fix to prevent pip duplication in Graded Group Sets - Updated dependencies \[[`f18c0d9b6f`](f18c0d9), [`a022e751d6`](a022e75), [`35fa9133db`](35fa913), [`54db3fd4bd`](54db3fd), [`97223334ea`](9722333), [`ae0538d0a7`](ae0538d), [`005e13d784`](005e13d), [`d99f1c0259`](d99f1c0), [`b1557c2a73`](b1557c2), [`dde985f3b5`](dde985f), [`7034844845`](7034844), [`8aa0a77886`](8aa0a77)]: - @khanacademy/perseus-linter@4.9.0 - @khanacademy/perseus-score@8.4.0 - @khanacademy/perseus-core@23.7.0 - @khanacademy/kmath@2.3.0 - @khanacademy/keypad-context@3.2.40 - @khanacademy/math-input@26.4.10 ## @khanacademy/perseus-core@23.7.0 ### Minor Changes - [#3405](#3405) [`54db3fd4bd`](54db3fd) Thanks [@benchristel](https://github.com/benchristel)! - `@khanacademy/perseus-core` now exports a `removeOrphanedWidgetsFromPerseusItem` function, which removes unreferenced widgets from a `PerseusItem`'s question and hints. - [#3351](#3351) [`005e13d784`](005e13d) Thanks [@handeyeco](https://github.com/handeyeco)! - Add scoring for AbsoluteValue - [#3348](#3348) [`b1557c2a73`](b1557c2) Thanks [@handeyeco](https://github.com/handeyeco)! - Add schema for AbsoluteValue graph - [#3345](#3345) [`dde985f3b5`](dde985f) Thanks [@ivyolamit](https://github.com/ivyolamit)! - Add tangent type definitions, this is the initial implementation for supporting Tangent graph in Interactive Graph - [#3376](#3376) [`8aa0a77886`](8aa0a77) Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! - Creation of new Types, Schema, and Kmath utilities for Exponential Graph ### Patch Changes - [#3357](#3357) [`ae0538d0a7`](ae0538d) Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Improve code documentation for all data-schema and user-input types ## @khanacademy/perseus-linter@4.9.0 ### Minor Changes - [#3381](#3381) [`f18c0d9b6f`](f18c0d9) Thanks [@anakaren-rojas](https://github.com/anakaren-rojas)! - Adds new linters for parsed objects - [#3395](#3395) [`97223334ea`](9722333) Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! - Implementation of Editor support for Exponential Graph ### Patch Changes - [#3396](#3396) [`35fa9133db`](35fa913) Thanks [@nishasy](https://github.com/nishasy)! - [Image] | (CX) | Add a linter warning for images with no size - Updated dependencies \[[`54db3fd4bd`](54db3fd), [`ae0538d0a7`](ae0538d), [`005e13d784`](005e13d), [`d99f1c0259`](d99f1c0), [`b1557c2a73`](b1557c2), [`dde985f3b5`](dde985f), [`8aa0a77886`](8aa0a77)]: - @khanacademy/perseus-core@23.7.0 - @khanacademy/kmath@2.3.0 ## @khanacademy/perseus-score@8.4.0 ### Minor Changes - [#3356](#3356) [`a022e751d6`](a022e75) Thanks [@ivyolamit](https://github.com/ivyolamit)! - Add tangent graph scoring to support the Tangent graph in Interactive Graph - [#3351](#3351) [`005e13d784`](005e13d) Thanks [@handeyeco](https://github.com/handeyeco)! - Add scoring for AbsoluteValue - [#3394](#3394) [`7034844845`](7034844) Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! - Implementation of new scoring logic for Exponential Graph ### Patch Changes - Updated dependencies \[[`54db3fd4bd`](54db3fd), [`ae0538d0a7`](ae0538d), [`005e13d784`](005e13d), [`d99f1c0259`](d99f1c0), [`b1557c2a73`](b1557c2), [`dde985f3b5`](dde985f), [`8aa0a77886`](8aa0a77)]: - @khanacademy/perseus-core@23.7.0 - @khanacademy/kmath@2.3.0 ## @khanacademy/keypad-context@3.2.40 ### Patch Changes - Updated dependencies \[[`54db3fd4bd`](54db3fd), [`ae0538d0a7`](ae0538d), [`005e13d784`](005e13d), [`b1557c2a73`](b1557c2), [`dde985f3b5`](dde985f), [`8aa0a77886`](8aa0a77)]: - @khanacademy/perseus-core@23.7.0 ## @khanacademy/math-input@26.4.10 ### Patch Changes - Updated dependencies \[[`54db3fd4bd`](54db3fd), [`ae0538d0a7`](ae0538d), [`005e13d784`](005e13d), [`b1557c2a73`](b1557c2), [`dde985f3b5`](dde985f), [`8aa0a77886`](8aa0a77)]: - @khanacademy/perseus-core@23.7.0 - @khanacademy/keypad-context@3.2.40
…ned (#3367) ## Summary: We have a number of images in our content that are saved without a size. If the image has a long description, it makes the "explore image" button available. If the "explore image" button is clicked and then image has no size, the modal shows up as completely empty. The image should show up in the modal even if there is no size. Updating that here. Issue: https://khanacademy.atlassian.net/browse/LEMS-3990 ## Test plan: `pnpm jest packages/perseus/src/widgets/image/components/explore-image-modal.test.tsx` Storybook - `/?path=/story/widgets-image-visual-regression-tests-interactions--long-description-clicked-state-with-no-size` - `/?path=/story/widgets-image-visual-regression-tests-interactions--long-description-clicked-state-with-no-size-large-image` - `/?path=/story/widgets-image-widget-demo--large-image-with-no-size-saved` - `/?path=/story/widgets-image-widget-demo--large-image-with-no-size-saved-scale-flag` | Before | After | | --- | --- | | <img width="1423" height="473" alt="Screenshot 2026-03-16 at 4 27 55 PM" src="https://github.com/user-attachments/assets/4044b4e4-6491-4fdc-b611-bd593b006051" /> | <img width="1427" height="761" alt="Screenshot 2026-03-16 at 4 28 12 PM" src="https://github.com/user-attachments/assets/1d5fd926-6818-4e90-ad9e-b1fb6e00b134" /> | | <img width="1427" height="391" alt="Screenshot 2026-03-16 at 4 28 00 PM" src="https://github.com/user-attachments/assets/1bfb5f33-e05a-4c0b-9446-67a3657a3c40" /> | <img width="1430" height="762" alt="Screenshot 2026-03-16 at 4 28 18 PM" src="https://github.com/user-attachments/assets/1b4fb9f3-f1a2-4827-aefa-6a5221cef1f5" /> | | Before - scale feature flag | after - scale feature flag | | <img width="1278" height="248" alt="Screenshot 2026-03-16 at 4 29 58 PM" src="https://github.com/user-attachments/assets/e4f5e50c-56d5-46fc-a2e8-f21f8c8a1432" /> | <img width="1281" height="755" alt="Screenshot 2026-03-16 at 4 30 12 PM" src="https://github.com/user-attachments/assets/58331eb2-7eeb-47d7-89f0-6e37ce31528d" /> | Author: nishasy Reviewers: nishasy, mark-fitzgerald, claude[bot], catandthemachines, ivyolamit Required Reviewers: Approved By: catandthemachines Checks: ⚪️ 1 check is neutral, ⏭️ 1 check has been skipped, ✅ 10 checks were successful Pull Request URL: #3367


Summary:
We have a number of images in our content that are saved without a size.
If the image has a long description, it makes the "explore image" button available.
If the "explore image" button is clicked and then image has no size, the modal
shows up as completely empty.
The image should show up in the modal even if there is no size. Updating that here.
Issue: https://khanacademy.atlassian.net/browse/LEMS-3990
Test plan:
pnpm jest packages/perseus/src/widgets/image/components/explore-image-modal.test.tsxStorybook
/?path=/story/widgets-image-visual-regression-tests-interactions--long-description-clicked-state-with-no-size/?path=/story/widgets-image-visual-regression-tests-interactions--long-description-clicked-state-with-no-size-large-image/?path=/story/widgets-image-widget-demo--large-image-with-no-size-saved/?path=/story/widgets-image-widget-demo--large-image-with-no-size-saved-scale-flag