From 6196375add76025fb1f30473912dd38cc001cca4 Mon Sep 17 00:00:00 2001 From: Nisha Yerunkar Date: Wed, 13 Mar 2024 14:14:27 -0700 Subject: [PATCH] View a locked point with mafs (#1067) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: The ability to view a locked points was added as part of [LEMS-1702](https://khanacademy.atlassian.net/browse/LEMS-1702), but now that we've switched to mafs, we need to implement viewing a point again with mafs. - Add a layer for locked figures to the mafs graph - Createa a new component for said locked feature - Add an interactive graph story to confirm that locked points can be viewed Issue: https://khanacademy.atlassian.net/browse/LEMS-1702 ## Test plan: - Open http://localhost:6006/?path=/story/perseus-widgets-interactive-graph--segment-with-mafs-and-locked-points - Confirm that red points are there at (-7, -7) and (2, -5) - Drag the points of the line segment over the locked points and confirm that the blue points sit on top of the red points. (Check the interactive layer is on top of the locked layer.) [LEMS-1702]: https://khanacademy.atlassian.net/browse/LEMS-1702?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ ### Story Screenshot 2024-03-12 at 3 17 13 PM Author: nishasy Reviewers: benchristel, nishasy, mark-fitzgerald, jeremywiebe, nedredmond Required Reviewers: Approved By: jeremywiebe, nedredmond Checks: ✅ Upload Coverage, ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Extract i18n strings (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ Jest Coverage (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1067 --- .changeset/shaggy-parrots-provide.md | 5 ++ .../interactive-graph-editor.stories.tsx | 33 +++++++++++ .../__stories__/interactive-graph.stories.tsx | 16 ++++++ .../interactive-graph.testdata.ts | 57 +++++++++++++++++++ .../__tests__/interactive-graph.test.ts | 42 ++++++++++++++ .../perseus/src/widgets/interactive-graph.tsx | 2 +- .../interactive-graphs/graph-locked-layer.tsx | 42 ++++++++++++++ .../widgets/interactive-graphs/mafs-graph.tsx | 9 +++ .../interactive-graphs/mafs-styles.css | 3 +- 9 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 .changeset/shaggy-parrots-provide.md create mode 100644 packages/perseus/src/widgets/interactive-graphs/graph-locked-layer.tsx diff --git a/.changeset/shaggy-parrots-provide.md b/.changeset/shaggy-parrots-provide.md new file mode 100644 index 0000000000..d110ac0c56 --- /dev/null +++ b/.changeset/shaggy-parrots-provide.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": minor +--- + +Can pass a point into `lockedFigures` into MafsGraph and a point will be displayed. diff --git a/packages/perseus-editor/src/widgets/__stories__/interactive-graph-editor.stories.tsx b/packages/perseus-editor/src/widgets/__stories__/interactive-graph-editor.stories.tsx index 6cec64257b..d09ac700ac 100644 --- a/packages/perseus-editor/src/widgets/__stories__/interactive-graph-editor.stories.tsx +++ b/packages/perseus-editor/src/widgets/__stories__/interactive-graph-editor.stories.tsx @@ -55,3 +55,36 @@ export const Controlled: StoryComponentType = { return ; }, }; + +/** + * Example of what the InteractiveGraphEditor experience is when using + * a Mafs-based InteractiveGraph. + */ +export const WithMafs: StoryComponentType = { + render: function Render() { + const reducer = (state, newState) => { + return { + ...state, + ...newState, + }; + }; + + const [state, dispatch] = React.useReducer(reducer, { + apiOptions: { + flags: { + mafs: { + segment: true, + }, + }, + }, + graph: { + type: "segment", + }, + correct: { + type: "segment", + }, + }); + + return ; + }, +}; diff --git a/packages/perseus/src/widgets/__stories__/interactive-graph.stories.tsx b/packages/perseus/src/widgets/__stories__/interactive-graph.stories.tsx index 70988f9853..68a074b2fc 100644 --- a/packages/perseus/src/widgets/__stories__/interactive-graph.stories.tsx +++ b/packages/perseus/src/widgets/__stories__/interactive-graph.stories.tsx @@ -11,6 +11,7 @@ import { polygonQuestion, rayQuestion, segmentQuestion, + segmentWithLockedPointsQuestion, sinusoidQuestion, } from "../__testdata__/interactive-graph.testdata"; @@ -56,6 +57,21 @@ export const Segment = (args: StoryArgs): React.ReactElement => ( ); +export const SegmentWithMafsAndLockedPoints = ( + args: StoryArgs, +): React.ReactElement => ( + +); + export const Sinusoid = (args: StoryArgs): React.ReactElement => ( ); diff --git a/packages/perseus/src/widgets/__testdata__/interactive-graph.testdata.ts b/packages/perseus/src/widgets/__testdata__/interactive-graph.testdata.ts index a1f85261cc..3abdac3a07 100644 --- a/packages/perseus/src/widgets/__testdata__/interactive-graph.testdata.ts +++ b/packages/perseus/src/widgets/__testdata__/interactive-graph.testdata.ts @@ -1,3 +1,5 @@ +import {color} from "@khanacademy/wonder-blocks-tokens"; + import type {Coord} from "../../interactive2/types"; import type {PerseusRenderer} from "../../perseus-types"; @@ -480,6 +482,61 @@ export const segmentQuestion: PerseusRenderer = { }, }; +export const segmentWithLockedPointsQuestion: PerseusRenderer = { + content: + "Line segment $\\overline{OG}$ is rotated $180^\\circ$ about the point $(-2,4)$. \n\n**Draw the image of this rotation using the interactive graph.**\n\n*The direction of a rotation by a positive angle is counter-clockwise.* \n\n[[☃ interactive-graph 1]]\n\n", + images: {}, + widgets: { + "interactive-graph 1": { + graded: true, + options: { + correct: { + coords: [ + [ + [-7, -7], + [2, -5], + ], + ], + type: "segment", + }, + graph: { + type: "segment", + }, + gridStep: [1, 1], + labels: ["x", "y"], + markings: "graph", + range: [ + [-10, 10], + [-10, 10], + ], + rulerLabel: "", + rulerTicks: 10, + showProtractor: false, + showRuler: false, + snapStep: [0.5, 0.5], + step: [1, 1], + lockedFigures: [ + { + type: "point", + coord: [-7, -7], + style: {stroke: color.red, fill: color.red}, + }, + { + type: "point", + coord: [2, -5], + style: {stroke: color.red, fill: color.red}, + }, + ], + }, + type: "interactive-graph", + version: { + major: 0, + minor: 0, + }, + }, + }, +}; + export const segmentQuestionDefaultCorrect: PerseusRenderer = { content: "Line segment $\\overline{OG}$ is rotated $180^\\circ$ about the point $(-2,4)$. \n\n**Draw the image of this rotation using the interactive graph.**\n\n*The direction of a rotation by a positive angle is counter-clockwise.* \n\n[[☃ interactive-graph 1]]\n\n", diff --git a/packages/perseus/src/widgets/__tests__/interactive-graph.test.ts b/packages/perseus/src/widgets/__tests__/interactive-graph.test.ts index a6612f734b..f0e45769f9 100644 --- a/packages/perseus/src/widgets/__tests__/interactive-graph.test.ts +++ b/packages/perseus/src/widgets/__tests__/interactive-graph.test.ts @@ -1,4 +1,5 @@ import {describe, beforeEach, it} from "@jest/globals"; +import {color} from "@khanacademy/wonder-blocks-tokens"; import {waitFor} from "@testing-library/react"; import {userEvent as userEventLib} from "@testing-library/user-event"; @@ -9,6 +10,7 @@ import {ApiOptions} from "../../perseus-api"; import { questionsAndAnswers, segmentQuestion, + segmentWithLockedPointsQuestion, segmentQuestionDefaultCorrect, } from "../__testdata__/interactive-graph.testdata"; @@ -178,3 +180,43 @@ describe("segment graph", () => { }); }); }); + +describe("locked layer", () => { + const apiOptions = {flags: {mafs: {segment: true}}}; + it("should render locked points", async () => { + // Arrange + const {container} = renderQuestion( + segmentWithLockedPointsQuestion, + apiOptions, + ); + + // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access + const points = container.querySelectorAll( + // Filter out the interactive points' circles + "circle:not([class*='movable-point'])", + ); + + // Act + + // Assert + expect(points).toHaveLength(2); + }); + + test("should render locked points with styles", async () => { + // Arrange + const {container} = renderQuestion( + segmentWithLockedPointsQuestion, + apiOptions, + ); + + // Act + // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access + const points = container.querySelectorAll( + "circle:not([class*='movable-point'])", + ); + + // Assert + expect(points[0]).toHaveStyle({fill: color.red, stroke: color.red}); + expect(points[1]).toHaveStyle({fill: color.red, stroke: color.red}); + }); +}); diff --git a/packages/perseus/src/widgets/interactive-graph.tsx b/packages/perseus/src/widgets/interactive-graph.tsx index ddeeb0a21d..3b0102dfd4 100644 --- a/packages/perseus/src/widgets/interactive-graph.tsx +++ b/packages/perseus/src/widgets/interactive-graph.tsx @@ -324,7 +324,7 @@ class InteractiveGraph extends React.Component { this.sinusoid = null; // eslint-disable-next-line react/no-string-refs // @ts-expect-error - TS2339 - Property 'reset' does not exist on type 'ReactInstance'. - this.refs.graph.reset(); + this.refs.graph?.reset(); }; setupGraphie: () => void = () => { diff --git a/packages/perseus/src/widgets/interactive-graphs/graph-locked-layer.tsx b/packages/perseus/src/widgets/interactive-graphs/graph-locked-layer.tsx new file mode 100644 index 0000000000..73e58ca49f --- /dev/null +++ b/packages/perseus/src/widgets/interactive-graphs/graph-locked-layer.tsx @@ -0,0 +1,42 @@ +import {UnreachableCaseError} from "@khanacademy/wonder-stuff-core"; +import {Point} from "mafs"; +import * as React from "react"; + +import type {LockedFigure} from "../../perseus-types"; + +type Props = { + lockedFigures: ReadonlyArray; +}; + +const GraphLockedLayer = (props: Props) => { + const {lockedFigures} = props; + return ( + <> + {lockedFigures.map((figure, index) => { + switch (figure.type) { + case "point": + const [x, y] = figure.coord; + return ( + + ); + } + + /** + * Devlopment-time future-proofing: This `never` should + * fail during type-checking if we add a new locked + * shape type and forget to handle it in any other + * switch case here. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + throw new UnreachableCaseError(figure.type); + })} + + ); +}; + +export default GraphLockedLayer; diff --git a/packages/perseus/src/widgets/interactive-graphs/mafs-graph.tsx b/packages/perseus/src/widgets/interactive-graphs/mafs-graph.tsx index d081a08357..18fb1dc318 100644 --- a/packages/perseus/src/widgets/interactive-graphs/mafs-graph.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/mafs-graph.tsx @@ -2,6 +2,7 @@ import {View} from "@khanacademy/wonder-blocks-core"; import {Mafs} from "mafs"; import * as React from "react"; +import GraphLockedLayer from "./graph-locked-layer"; import {SegmentGraph} from "./graphs"; import {Grid} from "./grid"; import {interactiveGraphReducer} from "./interactive-graph-reducer"; @@ -94,7 +95,15 @@ export const MafsGraph = React.forwardRef< width={width} height={height} > + {/* Background layer */} {!legacyGrid && } + + {/* Locked layer */} + {props.lockedFigures && ( + + )} + + {/* Interactive layer */} {renderGraph({ state, dispatch, diff --git a/packages/perseus/src/widgets/interactive-graphs/mafs-styles.css b/packages/perseus/src/widgets/interactive-graphs/mafs-styles.css index 63b8816db6..307be48f08 100644 --- a/packages/perseus/src/widgets/interactive-graphs/mafs-styles.css +++ b/packages/perseus/src/widgets/interactive-graphs/mafs-styles.css @@ -6,8 +6,7 @@ --mafs-bg: transparent; --mafs-fg: rgb(33, 36, 44); /* WB color.offBlack */ - --mafs-line-color: rgb(33, 36, 44, 0.64); /* WB color.offBlack64 */ - --grid-line-subdivision-color: rgba(33, 36, 44, 0.16); + --mafs-line-color: rgba(33, 36, 44, 0.16); /* WB color.offBlack16*/ --mafs-blue: #1865f2; /* WB color.blue */ --mafs-red: #d92916; /* WB color.red */