Skip to content

[ Interactive Graph | Vector Graph ] PR3: Rendering & Accessibility#3441

Merged
SonicScrewdriver merged 16 commits intomainfrom
LEMS-3971/vector-pr3
Apr 28, 2026
Merged

[ Interactive Graph | Vector Graph ] PR3: Rendering & Accessibility#3441
SonicScrewdriver merged 16 commits intomainfrom
LEMS-3971/vector-pr3

Conversation

@SonicScrewdriver
Copy link
Copy Markdown
Contributor

@SonicScrewdriver SonicScrewdriver commented Mar 31, 2026

Summary

This PR was created with the help of AI, albeit with heavy oversight and review.

This is part of a series of PRs implementing the vector graph type for the Interactive Graph widget:

PR1 – type definitions and schema
PR2 – state management
▶️ PR3 – rendering & accessibility (this PR)
PR4 – scoring
PR5 – editor support

Issue: LEMS-3971

  • Added the visual rendering component and screen reader support for the vector graph type
  • Extracted a shared PillDragHandle component from the asymptote-specific drag handle, now reused by both vector and exponential/logarithm graphs
  • The graph is now fully visible and interactive for learners — a directed line segment with an arrowhead at the tip, a draggable tip point, and a pill-shaped grab handle on the body for whole-vector translation. This replaces the "Not implemented" stubs from PR 1.

Changes:

New files:

  • graphs/vector.tsx — Rendering component composed from low-level primitives:
    • Body grab handleuseDraggable on a <g> element with transparent hit target, visible line with rounded caps (thickens on hover/focus/drag via CSS class .movable-vector-line), extension line to arrowhead, and a PillDragHandle at 1/3 along the line.
    • Tip pointuseControlPoint hook providing draggable dot with keyboard constraint that prevents overlap with the tail.
    • Arrowhead — rendered last in SVG order (on top of tip dot), positioned 1.5 grid steps past the tip along the vector direction with fixed strokeWidth={2}.
    • Grab handle blurs on drag end (onDragEnd) to prevent lingering focus ring. aria-live="polite" for screen reader announcements.
  • graphs/vector.test.tsx — 10 tests: 5 SR label tests, 2 describeVectorGraph tests, 3 getVectorTipKeyboardConstraint tests.
  • graphs/components/pill-drag-handle.tsx — Shared pill-shaped drag handle extracted from the former AsymptoteDragHandle. Supports arbitrary rotation angles via rotation prop. Used by vector (rotated to match vector angle) and asymptote (0° or 90°).
  • graphs/components/pill-drag-handle.test.tsx — 6 tests covering grip dots, focus ring, rotation, and default state.

Modified files:

  • graphs/components/movable-asymptote.tsx — Now imports PillDragHandle directly instead of the deleted AsymptoteDragHandle wrapper.
  • strings.ts — Four SR strings: srVectorGraph, srVectorPoints, srVectorTipPoint, srVectorGrabHandle.
  • mafs-graph.tsx — Replaced stub with renderVectorGraph() dispatch.
  • interactive-graph.tsx — Replaced stub with getVectorEquationString() showing component form ⟨dx, dy⟩.
  • mafs-styles.css — Added .movable-vector-line (stroke color, weight, rounded caps) and .pill-drag-handle-* classes. Removed old .movable-asymptote-handle-* classes.
  • interactive-graph.stories.tsx — Added Vector Storybook story.

Deleted files:

  • graphs/components/asymptote-drag-handle.tsx — Replaced by PillDragHandle used directly in MovableAsymptote.

Design decisions:

  • Shared PillDragHandle — Extracted from the asymptote-specific handle so vector, exponential, and logarithm graphs all use the same component. The rotation prop replaces the old orientation prop (0° = horizontal, 90° = vertical, arbitrary angles for vector).
  • No focus outline lines — Unlike MovableLine, the vector uses the drag handle pill's focus ring for focus indication, avoiding extra SVG lines on tab focus.
  • Line thickens on hover/focus/drag — Both visible lines apply the movable-dragging CSS class when active, but the arrowhead does not (fixed strokeWidth).
  • Arrowhead on top — Rendered after the tip point in SVG order so it stays visible over the thickened line.
  • CSS-driven styles — Line color (--mafs-blue), weight (--movable-line-stroke-weight), and caps (stroke-linecap: round) are in .movable-vector-line CSS class rather than inline styles.

Test plan:

  • pnpm tsc — no new type errors
  • pnpm test packages/perseus/src/widgets/interactive-graphs/graphs/ — all tests pass including 10 vector tests and 6 pill-drag-handle tests
  • Existing exponential and movable-asymptote tests pass (20 tests) — verified PillDragHandle migration doesn't regress
  • Verified in Storybook: default state, hover (line thickens, arrowhead doesn't), focus (pill focus ring, no extra lines), drag (grab handle blurs on release), arrowhead extends past tip at all angles
  • No existing interactive graph tests regress

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@SonicScrewdriver SonicScrewdriver changed the title docs(changeset): Creation of new Vector graph and subcomponents. [ Interactive Graph | Vector Graph ] PR3: Rendering & Accessibility Mar 31, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 31, 2026

🗄️ Schema Change: No Changes ✅

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 31, 2026

🛠️ Item Splitting: No Changes ✅

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 31, 2026

Size Change: +2.31 kB (+0.46%)

Total Size: 502 kB

📦 View Changed
Filename Size Change
packages/perseus/dist/es/index.js 198 kB +2.12 kB (+1.08%)
packages/perseus/dist/es/strings.js 8.46 kB +183 B (+2.21%)
ℹ️ View Unchanged
Filename Size
packages/kas/dist/es/index.js 20.6 kB
packages/keypad-context/dist/es/index.js 1 kB
packages/kmath/dist/es/index.js 6.36 kB
packages/math-input/dist/es/index.js 98.5 kB
packages/math-input/dist/es/strings.js 1.61 kB
packages/perseus-core/dist/es/index.item-splitting.js 12 kB
packages/perseus-core/dist/es/index.js 25.2 kB
packages/perseus-editor/dist/es/index.js 102 kB
packages/perseus-linter/dist/es/index.js 9.42 kB
packages/perseus-score/dist/es/index.js 9.7 kB
packages/perseus-utils/dist/es/index.js 403 B
packages/pure-markdown/dist/es/index.js 1.39 kB
packages/simple-markdown/dist/es/index.js 6.71 kB

compressed-size-action

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 31, 2026

npm Snapshot: Published

Good news!! We've packaged up the latest commit from this PR (8d5361d) and published it to npm. You
can install it using the tag PR3441.

Example:

pnpm add @khanacademy/perseus@PR3441

If you are working in Khan Academy's frontend, you can run the below command.

./dev/tools/bump_perseus_version.ts -t PR3441

If you are working in Khan Academy's webapp, you can run the below command.

./dev/tools/bump_perseus_version.js -t PR3441

@SonicScrewdriver SonicScrewdriver force-pushed the LEMS-3971/vector-pr3 branch 2 times, most recently from 77f5629 to 52d299e Compare April 7, 2026 19:52
@SonicScrewdriver SonicScrewdriver force-pushed the LEMS-3971/vector-pr2 branch 2 times, most recently from 86a5da9 to da4b606 Compare April 15, 2026 22:06
@SonicScrewdriver SonicScrewdriver force-pushed the LEMS-3971/vector-pr3 branch 2 times, most recently from bd44dd3 to 0c99f50 Compare April 15, 2026 22:26
Copy link
Copy Markdown
Contributor

@ivyolamit ivyolamit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding here my initial comment for the first pass. I'm happy to chat about the focus state behavior in chat.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something I fixed recently based on the playtest feedback on the drag handle focus, the expected behavior is when the user drags using the pill and the mouse hover away from the pill it will retain the focus state until user navigates away or navigate to another point whether it's using keyboard or mouse.

Vector Logarithm
Image Image

Suggestion: retaining the pill focus to have consistent behavior. I'm happy to discuss this with you 😉

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooo great point, I'll match that!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm I reviewed the play test feedback and I see a message from our designer specifically requesting that the drag handle should lose the focus state when the mouse moves out, which is how I implemented it. Should we check in with them before adjusting?

Copy link
Copy Markdown
Contributor

@ivyolamit ivyolamit Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also a nit pick: what you think of naming this drag-handle-pill.tsx DragHandlePill?
edit: it also matches how we're naming most things by using the action first.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an edge case, i'm not sure though but worth checking with content if there's a scenario if the point and arrow is 1 point apart or less, the user experience is not so great can't drag it all the time. I wonder if there's a mathematical limit of the distance of the line 🤔

Image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oooo makes sense to me! I didn't think about the order of the words so much while I was renaming it to be more generic.

I've also discussed the minimum distance with design/content but I haven't heard back yet. I might suggest that we leave this "as is" so that we can get feedback during the play test about minimum distances for the vector graph. ;) However, I can also implement a 2-grid-step minimum now if you feel we should make an executive decision

Copy link
Copy Markdown
Contributor

@ivyolamit ivyolamit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SonicScrewdriver here's my second pass, I think overall it's looking good. Let's chat about the items I pointed out for consistency. And please have a follow-up PR for the regression and state stories. Great work here! the drag handle looks simple, but i can see that you have done a great consideration here to make it look and work. 🎉

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: once all the PRs are merged for vector, can you please do a smoke test to ensure that the dashed asymptote and focus still works as expected.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: once this is release, please keep an eye out on the translation PR comment in frontend release and request for the new strings based on our process timeline is two weeks after it is requested, it should be still within timeline before our next internal and content play test.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't forget to add the regression story for this, as a followup PR after all 5 PRs are landed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops thought I had one!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a followup PR after all 5 PRs are landed please update/added new state behavior here

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an edge case, i'm not sure though but worth checking with content if there's a scenario if the point and arrow is 1 point apart or less, the user experience is not so great can't drag it all the time. I wonder if there's a mathematical limit of the distance of the line 🤔

Image

* Arbitrary angles are supported (e.g. for vector alignment).
* @default 0
*/
rotation?: number;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a comment, this addition is smart. good work! 🎉

focused: boolean;
};

// useControlArrowhead mirrors useControlPoint but renders a
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// useControlArrowhead mirrors useControlPoint but renders a
// Hook that mirrors useControlPoint but renders a

@ivyolamit
Copy link
Copy Markdown
Contributor

I just realized looking at the chromatic snapshot, you might need to rebase from main 😓 it tag the difference between the dashed drag handle. But I think this will go away once you do the interactive rebase once PR1 and PR2 landed.

image

Copy link
Copy Markdown
Contributor

@ivyolamit ivyolamit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on our chat, the minimum distance will be something to discuss with content and can be improved later in a followup PR if needed. Adding ✅ 👍🏼

@SonicScrewdriver SonicScrewdriver force-pushed the LEMS-3971/vector-pr2 branch 2 times, most recently from 724d32f to 046eb1c Compare April 27, 2026 23:09
@SonicScrewdriver SonicScrewdriver changed the base branch from LEMS-3971/vector-pr2 to main April 28, 2026 01:12
@SonicScrewdriver SonicScrewdriver merged commit de45286 into main Apr 28, 2026
11 checks passed
SonicScrewdriver added a commit that referenced this pull request Apr 28, 2026
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-core@25.0.0

### Major Changes

- [#3511](#3511)
[`15b0193db5`](15b0193)
Thanks [@benchristel](https://github.com/benchristel)! - Remove unused
`static` field from PerseusCSProgramWidgetOptions. Callers should update
test data that constructs `PerseusCSProgramWidgetOptions` to remove the
static field.

### Minor Changes

- [#3441](#3441)
[`de45286302`](de45286)
Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! -
Creation of new Vector graph and subcomponents.


- [#3433](#3433)
[`b4bb6e2f42`](b4bb6e2)
Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! -
Creation of initial types and stubs for Vector graph

### Patch Changes

- [#3434](#3434)
[`de2dda0258`](de2dda0)
Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! -
Implementation of state management logic for new Vector graph

- Updated dependencies
\[[`a5b9105c28`](a5b9105)]:
    -   @khanacademy/kas@2.2.2

## @khanacademy/perseus@77.3.0

### Minor Changes

- [#3441](#3441)
[`de45286302`](de45286)
Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! -
Creation of new Vector graph and subcomponents.


- [#3433](#3433)
[`b4bb6e2f42`](b4bb6e2)
Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! -
Creation of initial types and stubs for Vector graph


- [#3494](#3494)
[`8fb79829d0`](8fb7982)
Thanks [@ivyolamit](https://github.com/ivyolamit)! - Interactive Graph:
change asymptote line to dashed for both exponential and logarithm based
on user feedback

### Patch Changes

- [#3523](#3523)
[`c89cdbe2aa`](c89cdbe)
Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! -
Ensure the default coords for Exponential and Logarithm are slightly
further away from the asymptote.


- [#3508](#3508)
[`16f7f77ba1`](16f7f77)
Thanks [@nishasy](https://github.com/nishasy)! - [Image] | (CX) | Give
explore modal launcher a label saying it has description


- [#3496](#3496)
[`4d923417cd`](4d92341)
Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! - Add
fallback label to Numeric Inputs and a linter warning for missing labels
in the editor.


- [#3511](#3511)
[`15b0193db5`](15b0193)
Thanks [@benchristel](https://github.com/benchristel)! - Remove unused
`static` field from PerseusCSProgramWidgetOptions. Callers should update
test data that constructs `PerseusCSProgramWidgetOptions` to remove the
static field.


- [#3504](#3504)
[`b8178b52e7`](b8178b5)
Thanks [@nishasy](https://github.com/nishasy)! - [Image] | (a11y) | Add
aria-describedby to Explore Image modal


- [#3488](#3488)
[`3abc5e8277`](3abc5e8)
Thanks [@mark-fitzgerald](https://github.com/mark-fitzgerald)! - [Free
Response] Add visual regression tests


- [#3493](#3493)
[`11742c2cff`](11742c2)
Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! -
Implementing bug fix for jumping MovableLines in the Correct Answer
graph in the editor


- [#3483](#3483)
[`7794943ec7`](7794943)
Thanks [@nishasy](https://github.com/nishasy)! - [ColorSync] Numeric
Input - update visual regression tests


- [#3434](#3434)
[`de2dda0258`](de2dda0)
Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! -
Implementation of state management logic for new Vector graph

- Updated dependencies
\[[`de45286302`](de45286),
[`4d923417cd`](4d92341),
[`15b0193db5`](15b0193),
[`b4bb6e2f42`](b4bb6e2),
[`d3ef4dbcc2`](d3ef4db),
[`a5b9105c28`](a5b9105),
[`de2dda0258`](de2dda0)]:
    -   @khanacademy/perseus-core@25.0.0
    -   @khanacademy/perseus-linter@4.9.5
    -   @khanacademy/perseus-score@8.7.0
    -   @khanacademy/kas@2.2.2
    -   @khanacademy/keypad-context@3.2.44
    -   @khanacademy/kmath@2.4.2
    -   @khanacademy/math-input@26.4.15

## @khanacademy/perseus-editor@30.4.0

### Minor Changes

- [#3441](#3441)
[`de45286302`](de45286)
Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! -
Creation of new Vector graph and subcomponents.


- [#3443](#3443)
[`a3396604a7`](a339660)
Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! -
Creation of new Vector Graph Editor


- [#3433](#3433)
[`b4bb6e2f42`](b4bb6e2)
Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! -
Creation of initial types and stubs for Vector graph


- [#3492](#3492)
[`883133a28f`](883133a)
Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Add preview
data sanitizer to strip non-serializable values before postMessage

### Patch Changes

- [#3522](#3522)
[`19911cc966`](19911cc)
Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! - Label
absolute-value graph start coordinates as "Vertex" and "Arm" in the
editor instead of "Point 1" and "Point 2".


- [#3505](#3505)
[`1ab914fc41`](1ab914f)
Thanks [@nishasy](https://github.com/nishasy)! - [Image] | (CX) | Add
warning for large images


- [#3530](#3530)
[`b5e918e8b3`](b5e918e)
Thanks [@nishasy](https://github.com/nishasy)! - [Image] Update copy of
recalculate button in editor


- [#3434](#3434)
[`de2dda0258`](de2dda0)
Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! -
Implementation of state management logic for new Vector graph

- Updated dependencies
\[[`de45286302`](de45286),
[`c89cdbe2aa`](c89cdbe),
[`16f7f77ba1`](16f7f77),
[`4d923417cd`](4d92341),
[`15b0193db5`](15b0193),
[`b8178b52e7`](b8178b5),
[`b4bb6e2f42`](b4bb6e2),
[`d3ef4dbcc2`](d3ef4db),
[`3abc5e8277`](3abc5e8),
[`a5b9105c28`](a5b9105),
[`11742c2cff`](11742c2),
[`7794943ec7`](7794943),
[`de2dda0258`](de2dda0),
[`8fb79829d0`](8fb7982)]:
    -   @khanacademy/perseus@77.3.0
    -   @khanacademy/perseus-core@25.0.0
    -   @khanacademy/perseus-linter@4.9.5
    -   @khanacademy/perseus-score@8.7.0
    -   @khanacademy/kas@2.2.2
    -   @khanacademy/keypad-context@3.2.44
    -   @khanacademy/kmath@2.4.2
    -   @khanacademy/math-input@26.4.15

## @khanacademy/perseus-score@8.7.0

### Minor Changes

- [#3442](#3442)
[`d3ef4dbcc2`](d3ef4db)
Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! - Added
ability to score Vector Interactive Graphs

### Patch Changes

- Updated dependencies
\[[`de45286302`](de45286),
[`15b0193db5`](15b0193),
[`b4bb6e2f42`](b4bb6e2),
[`a5b9105c28`](a5b9105),
[`de2dda0258`](de2dda0)]:
    -   @khanacademy/perseus-core@25.0.0
    -   @khanacademy/kas@2.2.2
    -   @khanacademy/kmath@2.4.2

## @khanacademy/kas@2.2.2

### Patch Changes

- [#3503](#3503)
[`a5b9105c28`](a5b9105)
Thanks [@benchristel](https://github.com/benchristel)! - Expressions are
now compared more thoroughly. Now we always check that the expressions
evaluate the same with all variables bound to -1, 0, and 1. We also
check more randomly-chosen values: 28 instead of 12.

## @khanacademy/keypad-context@3.2.44

### Patch Changes

- Updated dependencies
\[[`de45286302`](de45286),
[`15b0193db5`](15b0193),
[`b4bb6e2f42`](b4bb6e2),
[`de2dda0258`](de2dda0)]:
    -   @khanacademy/perseus-core@25.0.0

## @khanacademy/kmath@2.4.2

### Patch Changes

- Updated dependencies
\[[`de45286302`](de45286),
[`15b0193db5`](15b0193),
[`b4bb6e2f42`](b4bb6e2),
[`de2dda0258`](de2dda0)]:
    -   @khanacademy/perseus-core@25.0.0

## @khanacademy/math-input@26.4.15

### Patch Changes

- Updated dependencies
\[[`de45286302`](de45286),
[`15b0193db5`](15b0193),
[`b4bb6e2f42`](b4bb6e2),
[`de2dda0258`](de2dda0)]:
    -   @khanacademy/perseus-core@25.0.0
    -   @khanacademy/keypad-context@3.2.44

## @khanacademy/perseus-linter@4.9.5

### Patch Changes

- [#3496](#3496)
[`4d923417cd`](4d92341)
Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! - Add
fallback label to Numeric Inputs and a linter warning for missing labels
in the editor.

- Updated dependencies
\[[`de45286302`](de45286),
[`15b0193db5`](15b0193),
[`b4bb6e2f42`](b4bb6e2),
[`a5b9105c28`](a5b9105),
[`de2dda0258`](de2dda0)]:
    -   @khanacademy/perseus-core@25.0.0
    -   @khanacademy/kas@2.2.2
    -   @khanacademy/kmath@2.4.2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants