From d9d130d5d89d5547612e3ff2430bb67655f90ea4 Mon Sep 17 00:00:00 2001 From: Nisha Yerunkar Date: Tue, 20 Aug 2024 14:32:28 -0700 Subject: [PATCH 1/6] [Locked labels] View locked labels in an Interactive Graph --- .changeset/violet-worms-invite.md | 6 +++ .../src/__stories__/flags-for-api-options.ts | 1 + .../src/components/__tests__/util.test.ts | 11 ++++++ .../locked-figures-section.tsx | 7 ++++ .../perseus-editor/src/components/util.ts | 10 +++++ packages/perseus/src/index.ts | 1 + packages/perseus/src/perseus-types.ts | 13 ++++++- packages/perseus/src/types.ts | 5 +++ .../__stories__/interactive-graph.stories.tsx | 9 +++++ .../interactive-graph.testdata.ts | 17 ++++++++ .../__tests__/interactive-graph.test.tsx | 22 +++++++++++ .../perseus/src/widgets/interactive-graph.tsx | 5 +++ .../graph-locked-labels-layer.tsx | 23 +++++++++++ .../interactive-graphs/graph-locked-layer.tsx | 6 +++ ...interactive-graph-question-builder.test.ts | 37 ++++++++++++++++++ .../interactive-graph-question-builder.ts | 22 +++++++++++ .../locked-figures/locked-label.tsx | 39 +++++++++++++++++++ .../widgets/interactive-graphs/mafs-graph.tsx | 10 ++++- .../stateful-mafs-graph.tsx | 1 + 19 files changed, 243 insertions(+), 2 deletions(-) create mode 100644 .changeset/violet-worms-invite.md create mode 100644 packages/perseus/src/widgets/interactive-graphs/graph-locked-labels-layer.tsx create mode 100644 packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-label.tsx diff --git a/.changeset/violet-worms-invite.md b/.changeset/violet-worms-invite.md new file mode 100644 index 0000000000..bf762f8c97 --- /dev/null +++ b/.changeset/violet-worms-invite.md @@ -0,0 +1,6 @@ +--- +"@khanacademy/perseus": minor +"@khanacademy/perseus-editor": minor +--- + +[Locked labels] View locked labels in an Interactive Graph diff --git a/packages/perseus-editor/src/__stories__/flags-for-api-options.ts b/packages/perseus-editor/src/__stories__/flags-for-api-options.ts index 479768047a..65e0285a57 100644 --- a/packages/perseus-editor/src/__stories__/flags-for-api-options.ts +++ b/packages/perseus-editor/src/__stories__/flags-for-api-options.ts @@ -15,6 +15,7 @@ export const flags = { // Locked figures flags "interactive-graph-locked-features-m2b": true, + "interactive-graph-locked-features-labels": true, // Start coords UI flags // TODO(LEMS-2228): Remove flags once this is fully released diff --git a/packages/perseus-editor/src/components/__tests__/util.test.ts b/packages/perseus-editor/src/components/__tests__/util.test.ts index 90d54044f7..ff293c6b6a 100644 --- a/packages/perseus-editor/src/components/__tests__/util.test.ts +++ b/packages/perseus-editor/src/components/__tests__/util.test.ts @@ -98,6 +98,17 @@ describe("getDefaultFigureForType", () => { directionalAxis: "x", }); }); + + test("should return a 'label' with default values", () => { + const figure = getDefaultFigureForType("label"); + expect(figure).toEqual({ + type: "label", + coord: [0, 0], + text: "", + color: "grayH", + size: "medium", + }); + }); }); describe("degreeToRadian", () => { diff --git a/packages/perseus-editor/src/components/graph-locked-figures/locked-figures-section.tsx b/packages/perseus-editor/src/components/graph-locked-figures/locked-figures-section.tsx index 2e48b1dcdd..6d1bdcb546 100644 --- a/packages/perseus-editor/src/components/graph-locked-figures/locked-figures-section.tsx +++ b/packages/perseus-editor/src/components/graph-locked-figures/locked-figures-section.tsx @@ -159,6 +159,13 @@ const LockedFiguresSection = (props: Props) => { {isExpanded && ( {figures?.map((figure, index) => { + if (figure.type === "label") { + // TODO(LEMS-1795): Add locked label settings. + // Remove this block once label locked figure + // settings are implemented. + return; + } + return ( ( /> ); +export const LockedLabel = (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 ca1d93ebf9..c4b0764792 100644 --- a/packages/perseus/src/widgets/__testdata__/interactive-graph.testdata.ts +++ b/packages/perseus/src/widgets/__testdata__/interactive-graph.testdata.ts @@ -783,6 +783,22 @@ export const segmentWithLockedFunction = ( .build(); }; +export const segmentWithLockedLabels: PerseusRenderer = + interactiveGraphQuestionBuilder() + .addLockedLabel("small \\frac{1}{2}", [-6, 2], { + color: "pink", + size: "small", + }) + .addLockedLabel("medium E_0 = mc^2", [1, 2], { + color: "blue", + size: "medium", + }) + .addLockedLabel("large \\sqrt{2a}", [-3, -2], { + color: "green", + size: "large", + }) + .build(); + export const segmentWithLockedFigures: PerseusRenderer = interactiveGraphQuestionBuilder() .addLockedPointAt(-7, -7) @@ -801,6 +817,7 @@ export const segmentWithLockedFigures: PerseusRenderer = .addLockedFunction("sin(x)", { color: "red", }) + .addLockedLabel("\\sqrt{\\frac{1}{2}}", [6, -5]) .build(); export const quadraticQuestion: PerseusRenderer = diff --git a/packages/perseus/src/widgets/__tests__/interactive-graph.test.tsx b/packages/perseus/src/widgets/__tests__/interactive-graph.test.tsx index 8c3a2a0eb2..e821794613 100644 --- a/packages/perseus/src/widgets/__tests__/interactive-graph.test.tsx +++ b/packages/perseus/src/widgets/__tests__/interactive-graph.test.tsx @@ -35,6 +35,7 @@ import { segmentWithLockedEllipses, segmentWithLockedEllipseWhite, segmentWithLockedFunction, + segmentWithLockedLabels, segmentWithLockedLineQuestion, segmentWithLockedPointsQuestion, segmentWithLockedPointsWithColorQuestion, @@ -859,4 +860,25 @@ describe("locked layer", () => { expect(PlotOfXMock).toHaveBeenCalledTimes(0); expect(PlotOfYMock).toHaveBeenCalledTimes(1); }); + + it("should render locked labels", async () => { + // Arrange + const {container} = renderQuestion(segmentWithLockedLabels, { + flags: { + mafs: { + segment: true, + "interactive-graph-locked-features-labels": true, + }, + }, + }); + + // Act + // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access + const labels = container.querySelectorAll(".locked-label"); + + // Assert + expect(labels).toHaveLength(3); + }); + + it("should render locked labels with style", async () => {}); }); diff --git a/packages/perseus/src/widgets/interactive-graph.tsx b/packages/perseus/src/widgets/interactive-graph.tsx index 93a7f1a0a5..2910408a5b 100644 --- a/packages/perseus/src/widgets/interactive-graph.tsx +++ b/packages/perseus/src/widgets/interactive-graph.tsx @@ -1821,6 +1821,11 @@ class InteractiveGraph extends React.Component { return ( ; +}; + +export default function GraphLockedLabelsLayer(props: Props) { + const {lockedFigures} = props; + + return lockedFigures.map((figure, i) => { + if (figure.type === "label") { + return ; + } + + // TODO(LEMS-2271): Add support for labels within + // other locked figure types + return null; + }); +} diff --git a/packages/perseus/src/widgets/interactive-graphs/graph-locked-layer.tsx b/packages/perseus/src/widgets/interactive-graphs/graph-locked-layer.tsx index a6959107ba..2f900a6f81 100644 --- a/packages/perseus/src/widgets/interactive-graphs/graph-locked-layer.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/graph-locked-layer.tsx @@ -59,6 +59,12 @@ const GraphLockedLayer = (props: Props) => { {...figure} /> ); + case "label": + // This is rendered outside the SVG element, since + // TeX cannot be rendered inside an SVG. + // See graph-locked-labels-layer.tsx for + // the component that renders these. + return null; default: /** * Devlopment-time future-proofing: This should diff --git a/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.test.ts b/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.test.ts index 63f90bc73a..d1190240da 100644 --- a/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.test.ts +++ b/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.test.ts @@ -961,4 +961,41 @@ describe("InteractiveGraphQuestionBuilder", () => { }, ]); }); + + it("adds a locked label", () => { + const question: PerseusRenderer = interactiveGraphQuestionBuilder() + .addLockedLabel("the text", [1, 2]) + .build(); + const graph = question.widgets["interactive-graph 1"]; + + expect(graph.options.lockedFigures).toEqual([ + { + type: "label", + text: "the text", + coord: [1, 2], + color: "grayH", + size: "medium", + }, + ]); + }); + + it("adds a locked label with options", () => { + const question: PerseusRenderer = interactiveGraphQuestionBuilder() + .addLockedLabel("some other text", [15, 2], { + color: "green", + size: "large", + }) + .build(); + const graph = question.widgets["interactive-graph 1"]; + + expect(graph.options.lockedFigures).toEqual([ + { + type: "label", + text: "some other text", + coord: [15, 2], + color: "green", + size: "large", + }, + ]); + }); }); diff --git a/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.ts b/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.ts index 0e0e70bc8a..e87624e54d 100644 --- a/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.ts +++ b/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.ts @@ -6,6 +6,7 @@ import type { LockedFigureColor, LockedFigureFillType, LockedFunctionType, + LockedLabelType, LockedLineType, LockedPointType, LockedPolygonType, @@ -383,6 +384,27 @@ class InteractiveGraphQuestionBuilder { return this; } + addLockedLabel( + text: string, + coord: Coord, + options?: { + color?: LockedFigureColor; + size?: "small" | "medium" | "large"; + }, + ) { + const lockedLabel: LockedLabelType = { + type: "label", + coord, + text, + color: "grayH", + size: "medium", + ...options, + }; + + this.addLockedFigure(lockedLabel); + return this; + } + private createLockedPoint( x: number, y: number, diff --git a/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-label.tsx b/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-label.tsx new file mode 100644 index 0000000000..5482acda20 --- /dev/null +++ b/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-label.tsx @@ -0,0 +1,39 @@ +import {font} from "@khanacademy/wonder-blocks-tokens"; + +import {getDependencies} from "../../../dependencies"; +import {lockedFigureColors, type LockedLabelType} from "../../../perseus-types"; +import {pointToPixel} from "../graphs/use-transform"; +import useGraphConfig from "../reducer/use-graph-config"; + +import type {GraphDimensions} from "../types"; + +export default function LockedLabel(props: LockedLabelType) { + const {coord, text, color, size} = props; + const {range, width, height} = useGraphConfig(); + + const graphInfo: GraphDimensions = { + range, + width, + height, + }; + + const [x, y] = pointToPixel(coord, graphInfo); + + const {TeX} = getDependencies(); + + // Move this all outside the SVG element + return ( + + {text} + + ); +} diff --git a/packages/perseus/src/widgets/interactive-graphs/mafs-graph.tsx b/packages/perseus/src/widgets/interactive-graphs/mafs-graph.tsx index cd5e6f3f45..f722cead68 100644 --- a/packages/perseus/src/widgets/interactive-graphs/mafs-graph.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/mafs-graph.tsx @@ -7,6 +7,7 @@ import AxisLabels from "./backgrounds/axis-labels"; import {AxisTickLabels} from "./backgrounds/axis-tick-labels"; import {Grid} from "./backgrounds/grid"; import {LegacyGrid} from "./backgrounds/legacy-grid"; +import GraphLockedLabelsLayer from "./graph-locked-labels-layer"; import GraphLockedLayer from "./graph-locked-layer"; import { LinearGraph, @@ -33,6 +34,7 @@ import "mafs/core.css"; import "./mafs-styles.css"; export type MafsGraphProps = { + showLabelsFlag?: boolean; box: [number, number]; backgroundImage?: InteractiveGraphProps["backgroundImage"]; lockedFigures?: InteractiveGraphProps["lockedFigures"]; @@ -100,6 +102,12 @@ export const MafsGraph = (props: MafsGraphProps) => { )} + {/* Locked labels layer */} + {props.showLabelsFlag && props.lockedFigures && ( + + )} { containerSizeClass={props.containerSizeClass} markings={props.markings} /> - {/* Locked layer */} + {/* Locked figures layer */} {props.lockedFigures && ( Date: Tue, 20 Aug 2024 16:06:30 -0700 Subject: [PATCH 2/6] Import React, write test --- .../__tests__/interactive-graph.test.tsx | 27 +++++++++++++++++-- .../locked-figures/locked-label.tsx | 1 + 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/perseus/src/widgets/__tests__/interactive-graph.test.tsx b/packages/perseus/src/widgets/__tests__/interactive-graph.test.tsx index e821794613..0c5cb8b65f 100644 --- a/packages/perseus/src/widgets/__tests__/interactive-graph.test.tsx +++ b/packages/perseus/src/widgets/__tests__/interactive-graph.test.tsx @@ -878,7 +878,30 @@ describe("locked layer", () => { // Assert expect(labels).toHaveLength(3); - }); - it("should render locked labels with style", async () => {}); + // content + expect(labels[0]).toHaveTextContent("small \\frac{1}{2}"); + expect(labels[1]).toHaveTextContent("medium E_0 = mc^2"); + expect(labels[2]).toHaveTextContent("large \\sqrt{2a}"); + + // styles + expect(labels[0]).toHaveStyle({ + color: lockedFigureColors["pink"], + fontSize: "14px", // small + left: "80px", + top: "160px", + }); + expect(labels[1]).toHaveStyle({ + color: lockedFigureColors["blue"], + fontSize: "16px", // medium + left: "220px", + top: "160px", + }); + expect(labels[2]).toHaveStyle({ + color: lockedFigureColors["green"], + fontSize: "20px", // large + left: "140px", + top: "240px", + }); + }); }); diff --git a/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-label.tsx b/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-label.tsx index 5482acda20..073315766e 100644 --- a/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-label.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-label.tsx @@ -1,4 +1,5 @@ import {font} from "@khanacademy/wonder-blocks-tokens"; +import * as React from "react"; import {getDependencies} from "../../../dependencies"; import {lockedFigureColors, type LockedLabelType} from "../../../perseus-types"; From 6ca6382ac3a8ce319a55c26f775a77401d2d720d Mon Sep 17 00:00:00 2001 From: Nisha Yerunkar Date: Tue, 20 Aug 2024 16:13:31 -0700 Subject: [PATCH 3/6] Replace m2b story with labels flag story --- .../src/__stories__/interactive-graph-editor.stories.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/perseus-editor/src/__stories__/interactive-graph-editor.stories.tsx b/packages/perseus-editor/src/__stories__/interactive-graph-editor.stories.tsx index f672376960..9ec83c3882 100644 --- a/packages/perseus-editor/src/__stories__/interactive-graph-editor.stories.tsx +++ b/packages/perseus-editor/src/__stories__/interactive-graph-editor.stories.tsx @@ -135,7 +135,7 @@ export const MafsWithLockedFiguresCurrent = (): React.ReactElement => { flags: { mafs: { ...flags.mafs, - "interactive-graph-locked-features-m2b": false, + "interactive-graph-locked-features-labels": false, }, }, }} @@ -152,13 +152,13 @@ MafsWithLockedFiguresCurrent.parameters = { }, }; -export const MafsWithLockedFiguresM2bFlag = (): React.ReactElement => { +export const MafsWithLockedLabelsFlag = (): React.ReactElement => { return ( ); }; -MafsWithLockedFiguresM2bFlag.parameters = { +MafsWithLockedLabelsFlag.parameters = { chromatic: { // Disabling because this isn't visually testing anything on the // initial load of the editor page. From 2795b665c832eee10343f064ef61feb0767e06e4 Mon Sep 17 00:00:00 2001 From: Nisha Yerunkar Date: Tue, 20 Aug 2024 16:41:07 -0700 Subject: [PATCH 4/6] [Locked Labels] Implement adding/editing/deleting a standalone locked label --- .changeset/angry-hounds-lick.md | 6 + .../locked-label-settings.stories.tsx | 48 ++++ .../locked-function-settings.test.tsx | 2 +- .../__tests__/locked-label-settings.test.tsx | 219 ++++++++++++++++++ .../locked-figure-select.tsx | 15 +- .../locked-figure-settings.tsx | 12 + .../locked-figures-section.tsx | 13 +- .../locked-label-settings.tsx | 156 +++++++++++++ .../src/widgets/interactive-graph-editor.tsx | 5 + 9 files changed, 464 insertions(+), 12 deletions(-) create mode 100644 .changeset/angry-hounds-lick.md create mode 100644 packages/perseus-editor/src/components/__stories__/locked-label-settings.stories.tsx create mode 100644 packages/perseus-editor/src/components/__tests__/locked-label-settings.test.tsx create mode 100644 packages/perseus-editor/src/components/graph-locked-figures/locked-label-settings.tsx diff --git a/.changeset/angry-hounds-lick.md b/.changeset/angry-hounds-lick.md new file mode 100644 index 0000000000..6650905267 --- /dev/null +++ b/.changeset/angry-hounds-lick.md @@ -0,0 +1,6 @@ +--- +"@khanacademy/perseus": patch +"@khanacademy/perseus-editor": minor +--- + +[Locked Labels] Implement adding/editing/deleting a standalone locked label diff --git a/packages/perseus-editor/src/components/__stories__/locked-label-settings.stories.tsx b/packages/perseus-editor/src/components/__stories__/locked-label-settings.stories.tsx new file mode 100644 index 0000000000..76aa4bcbde --- /dev/null +++ b/packages/perseus-editor/src/components/__stories__/locked-label-settings.stories.tsx @@ -0,0 +1,48 @@ +import * as React from "react"; + +import LockedLabelSettings from "../graph-locked-figures/locked-label-settings"; +import {getDefaultFigureForType} from "../util"; + +import type {Meta, StoryObj} from "@storybook/react"; + +export default { + title: "PerseusEditor/Components/Locked Label Settings", + component: LockedLabelSettings, +} as Meta; + +export const Default = (args): React.ReactElement => { + return ; +}; + +const defaultProps = { + ...getDefaultFigureForType("label"), + onChangeProps: () => {}, + onMove: () => {}, + onRemove: () => {}, +}; + +type StoryComponentType = StoryObj; + +// Set the default values in the control panel. +Default.args = defaultProps; + +export const Expanded: StoryComponentType = { + render: function Render() { + const [props, setProps] = React.useState(defaultProps); + + const handlePropsUpdate = (newProps) => { + setProps({ + ...props, + ...newProps, + }); + }; + + return ( + + ); + }, +}; diff --git a/packages/perseus-editor/src/components/__tests__/locked-function-settings.test.tsx b/packages/perseus-editor/src/components/__tests__/locked-function-settings.test.tsx index 642de1a439..feaa499409 100644 --- a/packages/perseus-editor/src/components/__tests__/locked-function-settings.test.tsx +++ b/packages/perseus-editor/src/components/__tests__/locked-function-settings.test.tsx @@ -37,7 +37,7 @@ describe("Locked Function Settings", () => { expect(titleText).toBeInTheDocument(); }); - describe("Heading interactions", () => { + describe("Header interactions", () => { test("should show the function's color and stroke by default", () => { // Arrange render(, { diff --git a/packages/perseus-editor/src/components/__tests__/locked-label-settings.test.tsx b/packages/perseus-editor/src/components/__tests__/locked-label-settings.test.tsx new file mode 100644 index 0000000000..afc42cc224 --- /dev/null +++ b/packages/perseus-editor/src/components/__tests__/locked-label-settings.test.tsx @@ -0,0 +1,219 @@ +import {RenderStateRoot} from "@khanacademy/wonder-blocks-core"; +import {render, screen} from "@testing-library/react"; +import {userEvent as userEventLib} from "@testing-library/user-event"; +import * as React from "react"; + +import LockedLabelSettings from "../graph-locked-figures/locked-label-settings"; +import {getDefaultFigureForType} from "../util"; + +import type {Props} from "../graph-locked-figures/locked-label-settings"; +import type {UserEvent} from "@testing-library/user-event"; + +const defaultProps = { + ...getDefaultFigureForType("label"), + onChangeProps: () => {}, + onMove: () => {}, + onRemove: () => {}, +} as Props; + +describe("Locked Label Settings", () => { + let userEvent: UserEvent; + const onChangeProps = jest.fn(); + + beforeEach(() => { + userEvent = userEventLib.setup({ + advanceTimers: jest.advanceTimersByTime, + }); + }); + + test("renders as expected with default values", () => { + // Arrange + + // Act + render(, { + wrapper: RenderStateRoot, + }); + + // Assert + const titleText = screen.getByText("Label (0, 0)"); + expect(titleText).toBeInTheDocument(); + }); + + describe("Header interactions", () => { + test("should show the correct coords in the header", () => { + // Arrange + render( + , + { + wrapper: RenderStateRoot, + }, + ); + + // Act + const titleText = screen.getByText("Label (1, 2)"); + + // Assert + expect(titleText).toBeInTheDocument(); + }); + + test("should show the text in the header", () => { + // Arrange + render( + , + { + wrapper: RenderStateRoot, + }, + ); + + // Act + const text = screen.getByText("something"); + + // Assert + expect(text).toBeInTheDocument(); + }); + + test("calls 'onToggle' when header is clicked", async () => { + // Arrange + const onToggle = jest.fn(); + render( + , + { + wrapper: RenderStateRoot, + }, + ); + + // Act + const header = screen.getByRole("button", { + name: "Label (0, 0) something", + }); + await userEvent.click(header); + + // Assert + expect(onToggle).toHaveBeenCalled(); + }); + }); + + describe("Settings interactions", () => { + test("calls 'onChangeProps' when coords are changed", async () => { + // Arrange + render( + , + { + wrapper: RenderStateRoot, + }, + ); + + // Act + const xCoordInput = screen.getByRole("spinbutton", { + name: "x coord", + }); + const yCoordInput = screen.getByRole("spinbutton", { + name: "y coord", + }); + + await userEvent.clear(xCoordInput); + await userEvent.type(xCoordInput, "1"); + + await userEvent.clear(yCoordInput); + await userEvent.type(yCoordInput, "2"); + + // Assert + expect(onChangeProps).toHaveBeenCalledTimes(2); + // Calls are not being accumulated because they're mocked. + expect(onChangeProps).toHaveBeenNthCalledWith(1, {coord: [1, 0]}); + expect(onChangeProps).toHaveBeenNthCalledWith(2, {coord: [0, 2]}); + }); + + test("calls 'onChangeProps' when text is changed", async () => { + // Arrange + render( + , + { + wrapper: RenderStateRoot, + }, + ); + + // Act + const textInput = screen.getByRole("textbox", { + name: "TeX", + }); + await userEvent.type(textInput, "x^2"); + + // Assert + // Assert + expect(onChangeProps).toHaveBeenCalledTimes(3); + // NOTE: Since the 'onChangeProps' function is being mocked, + // the equation doesn't get updated, + // and therefore the keystrokes don't accumulate. + // This is reflected in the calls to 'onChangeProps' being + // just 1 character at a time. + expect(onChangeProps).toHaveBeenNthCalledWith(1, {text: "x"}); + expect(onChangeProps).toHaveBeenNthCalledWith(2, {text: "^"}); + expect(onChangeProps).toHaveBeenNthCalledWith(3, {text: "2"}); + }); + + test("calls 'onChangeProps' when color is changed", async () => { + // Arrange + render( + , + { + wrapper: RenderStateRoot, + }, + ); + + // Act + // Change the color + const colorSwitch = screen.getByLabelText("color"); + await userEvent.click(colorSwitch); + const colorOption = screen.getByText("green"); + await userEvent.click(colorOption); + + // Assert + expect(onChangeProps).toHaveBeenCalledWith({color: "green"}); + }); + + test("calls 'onChangeProps' when size is changed", async () => { + // Arrange + render( + , + { + wrapper: RenderStateRoot, + }, + ); + + // Act + // Change the color + const sizeSelect = screen.getByLabelText("size"); + await userEvent.click(sizeSelect); + const sizeOption = screen.getByText("small"); + await userEvent.click(sizeOption); + + // Assert + expect(onChangeProps).toHaveBeenCalledWith({size: "small"}); + }); + }); +}); diff --git a/packages/perseus-editor/src/components/graph-locked-figures/locked-figure-select.tsx b/packages/perseus-editor/src/components/graph-locked-figures/locked-figure-select.tsx index c40d030316..4094ee9d99 100644 --- a/packages/perseus-editor/src/components/graph-locked-figures/locked-figure-select.tsx +++ b/packages/perseus-editor/src/components/graph-locked-figures/locked-figure-select.tsx @@ -14,6 +14,10 @@ import * as React from "react"; type Props = { // TODO(LEMS-2107): Remove this prop once the M2b flag is fully rolled out. showM2bFeatures: boolean; + // Whether to show the locked labels in the locked figure settings. + // TODO(LEMS-2274): Remove this prop once the label flag is + // sfully rolled out. + showLabelsFlag?: boolean; id: string; onChange: (value: string) => void; }; @@ -22,9 +26,12 @@ const LockedFigureSelect = (props: Props) => { const {id, onChange} = props; const figureTypes = ["point", "line", "vector", "ellipse", "polygon"]; - const figureTypesCurrent = props.showM2bFeatures - ? [...figureTypes, "function"] - : figureTypes; + if (props.showM2bFeatures) { + figureTypes.push("function"); + } + if (props.showLabelsFlag) { + figureTypes.push("label"); + } return ( @@ -32,7 +39,7 @@ const LockedFigureSelect = (props: Props) => { menuText="Add locked figure" style={styles.addElementSelect} > - {figureTypesCurrent.map((figureType) => ( + {figureTypes.map((figureType) => ( { @@ -76,6 +83,11 @@ const LockedFigureSettings = (props: Props) => { return ; } break; + case "label": + if (props.showLabelsFlag) { + return ; + } + break; } return null; diff --git a/packages/perseus-editor/src/components/graph-locked-figures/locked-figures-section.tsx b/packages/perseus-editor/src/components/graph-locked-figures/locked-figures-section.tsx index 6d1bdcb546..be786e19bc 100644 --- a/packages/perseus-editor/src/components/graph-locked-figures/locked-figures-section.tsx +++ b/packages/perseus-editor/src/components/graph-locked-figures/locked-figures-section.tsx @@ -24,6 +24,10 @@ type Props = { // Whether to show the M2b features in the locked figure settings. // TODO(LEMS-2107): Remove this prop once the M2b flag is fully rolled out. showM2bFeatures: boolean; + // Whether to show the locked labels in the locked figure settings. + // TODO(LEMS-2274): Remove this prop once the label flag is + // sfully rolled out. + showLabelsFlag?: boolean; figures?: Array; onChange: (props: Partial) => void; }; @@ -159,17 +163,11 @@ const LockedFiguresSection = (props: Props) => { {isExpanded && ( {figures?.map((figure, index) => { - if (figure.type === "label") { - // TODO(LEMS-1795): Add locked label settings. - // Remove this block once label locked figure - // settings are implemented. - return; - } - return ( { const newExpanded = [...expandedStates]; @@ -190,6 +188,7 @@ const LockedFiguresSection = (props: Props) => { diff --git a/packages/perseus-editor/src/components/graph-locked-figures/locked-label-settings.tsx b/packages/perseus-editor/src/components/graph-locked-figures/locked-label-settings.tsx new file mode 100644 index 0000000000..b9482c5cf7 --- /dev/null +++ b/packages/perseus-editor/src/components/graph-locked-figures/locked-label-settings.tsx @@ -0,0 +1,156 @@ +/** + * LockedLabelSettings is a component that allows the user to edit the + * settings of specifically a locked label on the graph within the + * Interactive Graph widget. + * + * Used in the interactive graph editor's locked figures section. + */ +import { + lockedFigureColors, + type LockedFigure, + type LockedFigureColor, + type LockedLabelType, +} from "@khanacademy/perseus"; +import {View} from "@khanacademy/wonder-blocks-core"; +import {OptionItem, SingleSelect} from "@khanacademy/wonder-blocks-dropdown"; +import {TextField} from "@khanacademy/wonder-blocks-form"; +import {Strut} from "@khanacademy/wonder-blocks-layout"; +import {spacing, color as wbColor} from "@khanacademy/wonder-blocks-tokens"; +import {LabelLarge, LabelMedium} from "@khanacademy/wonder-blocks-typography"; +import {StyleSheet} from "aphrodite"; +import * as React from "react"; + +import CoordinatePairInput from "../coordinate-pair-input"; +import PerseusEditorAccordion from "../perseus-editor-accordion"; + +import ColorSelect from "./color-select"; +import LockedFigureSettingsActions from "./locked-figure-settings-actions"; + +import type {LockedFigureSettingsCommonProps} from "./locked-figure-settings"; + +export type Props = LockedLabelType & + LockedFigureSettingsCommonProps & { + /** + * Called when the props (coord, color, etc.) are updated. + */ + onChangeProps: (newProps: Partial) => void; + }; + +export default function LockedLabelSettings(props: Props) { + const {coord, color, size, text, onChangeProps} = props; + + return ( + + + Label ({coord[0]}, {coord[1]}) + + + {text !== "" && ( + + {text} + + )} + + } + > + {/* Coord settings */} + { + onChangeProps({coord: newCoords}); + }} + style={styles.spaceUnder} + /> + + {/* Text settings */} + + TeX + + + onChangeProps({ + text: newValue, + }) + } + /> + + + + {/* Color settings */} + { + onChangeProps({color: newColor}); + }} + style={styles.spaceUnder} + /> + + + {/* Size settings */} + + size + + + onChangeProps({ + size: newValue, + }) + } + // Placeholder is required, but never gets used since + // we have a label for the select. + placeholder="" + > + + + + + + + + {/* Actions */} + + + ); +} +const styles = StyleSheet.create({ + accordionHeaderContainer: { + // Stop the label summary from wrapping. + whiteSpace: "nowrap", + }, + accordionHeader: { + padding: spacing.xxxSmall_4, + marginInlineEnd: spacing.xSmall_8, + borderRadius: spacing.xxxSmall_4, + textOverflow: "ellipsis", + overflow: "hidden", + }, + row: { + display: "flex", + flexDirection: "row", + alignItems: "center", + // Allow truncation, stop bleeding over the edge. + minWidth: 0, + }, + spaceUnder: { + marginBottom: spacing.xSmall_8, + }, +}); diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor.tsx index 914b0be911..03e0d0e2a0 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor.tsx @@ -605,6 +605,11 @@ class InteractiveGraphEditor extends React.Component { "interactive-graph-locked-features-m2b" ] } + showLabelsFlag={ + this.props.apiOptions?.flags?.mafs?.[ + "interactive-graph-locked-features-labels" + ] + } figures={this.props.lockedFigures} onChange={this.props.onChange} /> From 2fbb312e6e8a2917fff9d6286497290c8af2480e Mon Sep 17 00:00:00 2001 From: Nisha Yerunkar Date: Thu, 22 Aug 2024 11:42:13 -0700 Subject: [PATCH 5/6] streamline code --- .../locked-figures/locked-label.tsx | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-label.tsx b/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-label.tsx index 073315766e..93ca566b41 100644 --- a/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-label.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-label.tsx @@ -6,19 +6,10 @@ import {lockedFigureColors, type LockedLabelType} from "../../../perseus-types"; import {pointToPixel} from "../graphs/use-transform"; import useGraphConfig from "../reducer/use-graph-config"; -import type {GraphDimensions} from "../types"; - export default function LockedLabel(props: LockedLabelType) { const {coord, text, color, size} = props; - const {range, width, height} = useGraphConfig(); - - const graphInfo: GraphDimensions = { - range, - width, - height, - }; - const [x, y] = pointToPixel(coord, graphInfo); + const [x, y] = pointToPixel(coord, useGraphConfig()); const {TeX} = getDependencies(); From 388a690e835e26462311eaa71a2cebfd97d091d9 Mon Sep 17 00:00:00 2001 From: Nisha Yerunkar Date: Thu, 22 Aug 2024 11:53:58 -0700 Subject: [PATCH 6/6] Destructure props more consistently. Streamline test. --- .../__tests__/locked-label-settings.test.tsx | 15 +++++++----- .../locked-label-settings.tsx | 23 ++++++++++++++----- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/packages/perseus-editor/src/components/__tests__/locked-label-settings.test.tsx b/packages/perseus-editor/src/components/__tests__/locked-label-settings.test.tsx index afc42cc224..a89ca0686e 100644 --- a/packages/perseus-editor/src/components/__tests__/locked-label-settings.test.tsx +++ b/packages/perseus-editor/src/components/__tests__/locked-label-settings.test.tsx @@ -135,8 +135,10 @@ describe("Locked Label Settings", () => { // Assert expect(onChangeProps).toHaveBeenCalledTimes(2); // Calls are not being accumulated because they're mocked. - expect(onChangeProps).toHaveBeenNthCalledWith(1, {coord: [1, 0]}); - expect(onChangeProps).toHaveBeenNthCalledWith(2, {coord: [0, 2]}); + expect(onChangeProps.mock.calls).toEqual([ + [{coord: [1, 0]}], + [{coord: [0, 2]}], + ]); }); test("calls 'onChangeProps' when text is changed", async () => { @@ -157,7 +159,6 @@ describe("Locked Label Settings", () => { }); await userEvent.type(textInput, "x^2"); - // Assert // Assert expect(onChangeProps).toHaveBeenCalledTimes(3); // NOTE: Since the 'onChangeProps' function is being mocked, @@ -165,9 +166,11 @@ describe("Locked Label Settings", () => { // and therefore the keystrokes don't accumulate. // This is reflected in the calls to 'onChangeProps' being // just 1 character at a time. - expect(onChangeProps).toHaveBeenNthCalledWith(1, {text: "x"}); - expect(onChangeProps).toHaveBeenNthCalledWith(2, {text: "^"}); - expect(onChangeProps).toHaveBeenNthCalledWith(3, {text: "2"}); + expect(onChangeProps.mock.calls).toEqual([ + [{text: "x"}], + [{text: "^"}], + [{text: "2"}], + ]); }); test("calls 'onChangeProps' when color is changed", async () => { diff --git a/packages/perseus-editor/src/components/graph-locked-figures/locked-label-settings.tsx b/packages/perseus-editor/src/components/graph-locked-figures/locked-label-settings.tsx index b9482c5cf7..f6616ccee9 100644 --- a/packages/perseus-editor/src/components/graph-locked-figures/locked-label-settings.tsx +++ b/packages/perseus-editor/src/components/graph-locked-figures/locked-label-settings.tsx @@ -37,12 +37,23 @@ export type Props = LockedLabelType & }; export default function LockedLabelSettings(props: Props) { - const {coord, color, size, text, onChangeProps} = props; + const { + type, + coord, + color, + size, + text, + expanded, + onChangeProps, + onMove, + onRemove, + onToggle, + } = props; return ( @@ -124,9 +135,9 @@ export default function LockedLabelSettings(props: Props) { {/* Actions */} );