Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Interactive Graph] Polygon side snapping is keyboard accessible. #2270

Open
wants to merge 18 commits into
base: main
Choose a base branch
from

Conversation

catandthemachines
Copy link
Member

@catandthemachines catandthemachines commented Feb 25, 2025

Summary:

Updating Polygon interactive graph implementation to have keyboard accessible side snapping. This is done by hooking up the side snapping behavior into a constraint function to make to keyboard arrow keys.

Issue: LEMS-2243

Test plan:

  • Go to: /?path=/story/perseuseditor-widgets-interactive-graph--interactive-graph-polygon
  • Set Snap to: to side measures
  • Use your keyboard (tab and arrow keys) to move the various polygon points.
  • Notice they now move!

@catandthemachines catandthemachines self-assigned this Feb 25, 2025
Copy link
Contributor

github-actions bot commented Feb 25, 2025

Size Change: +1.34 kB (+0.15%)

Total Size: 874 kB

Filename Size Change
packages/perseus/dist/es/index.js 369 kB +1.34 kB (+0.36%)
ℹ️ View Unchanged
Filename Size
packages/kas/dist/es/index.js 39.7 kB
packages/keypad-context/dist/es/index.js 760 B
packages/kmath/dist/es/index.js 11.1 kB
packages/math-input/dist/es/index.js 78.2 kB
packages/math-input/dist/es/strings.js 1.79 kB
packages/perseus-core/dist/es/index.js 29.8 kB
packages/perseus-editor/dist/es/index.js 276 kB
packages/perseus-linter/dist/es/index.js 22.8 kB
packages/perseus-score/dist/es/index.js 20.6 kB
packages/perseus/dist/es/strings.js 6.74 kB
packages/pure-markdown/dist/es/index.js 4.14 kB
packages/simple-markdown/dist/es/index.js 13.1 kB

compressed-size-action

Copy link
Contributor

github-actions bot commented Feb 25, 2025

npm Snapshot: Published

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

Example:

pnpm add @khanacademy/perseus@PR2270

If you are working in Khan Academy's webapp, you can run:

./dev/tools/bump_perseus_version.sh -t PR2270

const constrain = ["angles", "sides"].includes(snapTo)
? (p) => p
: (p) => snap(snapStep, p);
const constrain: KeyboardMovementConstraint =
Copy link
Contributor

@nishasy nishasy Feb 27, 2025

Choose a reason for hiding this comment

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

Nit: Can this be a function named getConstraintFn that includes a switch statement for the snapTo type? I think that might be easier to follow for engineers looking at this in the future.

Something like

function getConstraintFn(): KeyboardMovementConstraint {
  switch(snapTo) {
    case "angles":
      return p => p
    case "sides":
      return getSideSnapConstraint(...)
    default: // "grid"
      return p => snap(snapStep, p);
  }
}

And then you can pass just this function into constrain below.

@@ -683,3 +689,60 @@ function describePolygonGraph(
srPolygonInteractiveElements,
};
}

export function getSideSnapConstraint(
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a test for this?

@@ -66,6 +66,22 @@ export function bound({
return clampToBox(boundingBox, point);
}

// Returns true if the point is within the range of the graph.
export function isInBound({
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a test for this?

@nishasy
Copy link
Contributor

nishasy commented Feb 27, 2025

Question: I see this is allowing approximate sides too. Is that intended?

image

@catandthemachines
Copy link
Member Author

Question: I see this is allowing approximate sides too. Is that intended?

image

Yes, this is how the functionality works currently. It will show that when it's not exactly a whole number, but it's very close.

Copy link
Member

@benchristel benchristel left a comment

Choose a reason for hiding this comment

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

This looks good overall! Thanks for this huge accessibility improvement!

Requesting changes because coords is being mutated in the reducer; we should make a copy instead. I left a few other suggestions inline too.

// Takes the destination point and makes sure it is within the bounds of the graph
// SnapStep is [0, 0] because we don't want to snap to the grid
coordsCopy[index] = bound({
coords[index] = bound({
Copy link
Member

Choose a reason for hiding this comment

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

This mutates the input coords array, which is likely to cause problems at some point because React assumes immutability. We should use setAtIndex to immutably transform the array.

const targetCoords = setAtIndex({
    array: coords,
    index,
    newValue: bound({point: destinationPoint, range, snapStep: [0, 0]}),
});

Copy link
Member Author

Choose a reason for hiding this comment

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

I adjusted the implementation to have the components/functions that call this one to pass in a coordsCopy, so there's no issue about mutating the shared state. Though as I'm looking at it this might cause issues down the line, I'll adjust this to make sure the function make's it's own copy so other functions just need to pass in the coord state. 😉

Comment on lines +77 to +82
return (
point[0] >= range[0][0] &&
point[0] <= range[0][1] &&
point[1] >= range[1][0] &&
point[1] <= range[1][1]
);
Copy link
Member

Choose a reason for hiding this comment

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

FWIW we have constants in interactive-graphs/math/interval.ts and interactive-graphs/math/coordinates.ts that could potentially make this easier to read:

return (
    point[X] >= range[X][MIN] &&
    point[X] <= range[X][MAX] &&
    point[Y] >= range[Y][MIN] &&
    point[Y] <= range[Y][MAX]
);

Though I don't think we use these constants in many places, so maybe they're more surprising than helpful.

It also might be worth moving this function to interactive-graphs/math/box.ts since that is the module that knows all about ranges and points.

Copy link
Member Author

Choose a reason for hiding this comment

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

That sounds good! One question I have is if we have a function/utility that already check this? I know we have the bound function that returns a point within the bounds of our graph, but I could not find a function that returns true or false if a point is outside the graph. 🤔

? (p) => p
: (p) => snap(snapStep, p);
const constrain: KeyboardMovementConstraint =
snapTo === "angles" ? (p) => p : (p) => snap(snapStep, p);
Copy link
Member

Choose a reason for hiding this comment

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

Could we use a switch statement to get the constrain value? Currently, the logic is split between two ternaries that are a couple hundred lines apart, and I think that will confuse future readers.

function getKeyboardMovementConstraintForPoint(
  pointIndex: number,
): KeyboardMovementConstraint {
  switch (snapTo) {
    case "grid":
      return (p) => snap(snapStep, p);
    case "angles":
      return (p) => p;
    case "sides":
      return getSideSnapConstraint(points, pointIndex, graphConfig.range);
    default:
      throw new UnreachableCaseError(snapTo);
  }
}

Complication: it looks like the constrain on line 102 is used in the useDraggable below, which I think is for moving the whole polygon. So we'd also need:

function getConstrainFunctionForWholePolygon(): (point: vec.Vector2) => vec.Vector2 {
  return snapTo === "angles" ? (p) => p : (p) => snap(snapStep, p);
}

@@ -66,6 +66,22 @@ export function bound({
return clampToBox(boundingBox, point);
}

// Returns true if the point is within the range of the graph.
export function isInBound({
Copy link
Member Author

Choose a reason for hiding this comment

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

Is there another function that currently exists to check if a point is in bounds? I know we have the function bound which moves a point to be in bounds, but it doesn't seem like we have one as a simple boolean.

Copy link
Member Author

Choose a reason for hiding this comment

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

I really want these test examples to be on graph markings. It makes it easier to debug and troubleshoot. ❤️

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure why this file changed. I'm going to take a look and make sure this is correct.

// Takes the destination point and makes sure it is within the bounds of the graph
// SnapStep is [0, 0] because we don't want to snap to the grid
coordsCopy[index] = bound({
coords[index] = bound({
Copy link
Member Author

Choose a reason for hiding this comment

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

I adjusted the implementation to have the components/functions that call this one to pass in a coordsCopy, so there's no issue about mutating the shared state. Though as I'm looking at it this might cause issues down the line, I'll adjust this to make sure the function make's it's own copy so other functions just need to pass in the coord state. 😉

Comment on lines +77 to +82
return (
point[0] >= range[0][0] &&
point[0] <= range[0][1] &&
point[1] >= range[1][0] &&
point[1] <= range[1][1]
);
Copy link
Member Author

Choose a reason for hiding this comment

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

That sounds good! One question I have is if we have a function/utility that already check this? I know we have the bound function that returns a point within the bounds of our graph, but I could not find a function that returns true or false if a point is outside the graph. 🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants