Skip to content

Commit

Permalink
[Locked Figure Labels] Add/edit/delete locked function labels
Browse files Browse the repository at this point in the history
  • Loading branch information
nishasy committed Sep 23, 2024
1 parent e781512 commit 8d62e1c
Show file tree
Hide file tree
Showing 3 changed files with 256 additions and 5 deletions.
6 changes: 6 additions & 0 deletions .changeset/rude-lamps-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@khanacademy/perseus": minor
"@khanacademy/perseus-editor": minor
---

[Locked Figure Labels] Add/edit/delete locked function labels
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {render, screen} from "@testing-library/react";
import {userEvent as userEventLib} from "@testing-library/user-event";
import * as React from "react";

import {flags} from "../../../__stories__/flags-for-api-options";

// Disabling the following linting error because the import is needed for mocking purposes.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import examples from "./locked-function-examples";
Expand All @@ -13,12 +15,21 @@ import type {Props} from "./locked-function-settings";
import type {UserEvent} from "@testing-library/user-event";

const defaultProps = {
flags: {
...flags,
mafs: {
...flags.mafs,
"locked-ellipse-settings": true,
},
},
...getDefaultFigureForType("function"),
onChangeProps: () => {},
onMove: () => {},
onRemove: () => {},
} as Props;

const defaultLabel = getDefaultFigureForType("label");

const exampleEquationsMock = {
foo: ["bar", "zot"],
};
Expand Down Expand Up @@ -178,7 +189,10 @@ describe("Locked Function Settings", () => {
await userEvent.click(colorOption);

// Assert
expect(onChangeProps).toHaveBeenCalledWith({color: "green"});
expect(onChangeProps).toHaveBeenCalledWith({
color: "green",
labels: [],
});
});

test("calls 'onChangeProps' when stroke style is changed", async () => {
Expand Down Expand Up @@ -506,5 +520,138 @@ describe("Locked Function Settings", () => {
expect(onChangeProps).toHaveBeenCalledWith({equation: "bar"});
});
});

describe("Labels", () => {
test("Updates the label color when the function color changes", async () => {
// Arrange
const onChangeProps = jest.fn();
render(
<LockedFunctionSettings
{...defaultProps}
color="green"
labels={[
{
...defaultLabel,
color: "green",
},
]}
onChangeProps={onChangeProps}
/>,
{wrapper: RenderStateRoot},
);

// Act
const colorSelect = screen.getByLabelText("color");
await userEvent.click(colorSelect);
const colorOption = screen.getByText("pink");
await userEvent.click(colorOption);

// Assert
expect(onChangeProps).toHaveBeenCalledWith({
color: "pink",
labels: [
{
...defaultLabel,
color: "pink",
},
],
});
});

test("Updates the label when the label text changes", async () => {
// Arrange
const onChangeProps = jest.fn();
render(
<LockedFunctionSettings
{...defaultProps}
labels={[
{
...defaultLabel,
text: "label text",
},
]}
onChangeProps={onChangeProps}
/>,
{wrapper: RenderStateRoot},
);

// Act
const labelText = screen.getByLabelText("TeX");
await userEvent.type(labelText, "!");

// Assert
expect(onChangeProps).toHaveBeenCalledWith({
labels: [{...defaultLabel, text: "label text!"}],
});
});

test("Removes label when delete button is clicked", async () => {
// Arrange
const onChangeProps = jest.fn();
render(
<LockedFunctionSettings
{...defaultProps}
labels={[
{
...defaultLabel,
text: "label text",
},
]}
onChangeProps={onChangeProps}
/>,
{wrapper: RenderStateRoot},
);

// Act
const deleteButton = screen.getByRole("button", {
name: "Delete locked label",
});
await userEvent.click(deleteButton);

// Assert
expect(onChangeProps).toHaveBeenCalledWith({
labels: [],
});
});

test("Adds a new label when the add label button is clicked", async () => {
// Arrange
const onChangeProps = jest.fn();
render(
<LockedFunctionSettings
{...defaultProps}
labels={[
{
...defaultLabel,
text: "label text",
},
]}
onChangeProps={onChangeProps}
/>,
{wrapper: RenderStateRoot},
);

// Act
const addLabelButton = screen.getByRole("button", {
name: "Add visible label",
});
await userEvent.click(addLabelButton);

// Assert
expect(onChangeProps).toHaveBeenCalledWith({
labels: [
{
...defaultLabel,
text: "label text",
},
{
...defaultLabel,
// One unit down vertically from the first label.
coord: [0, -1],
},
],
});
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*
* Used in the interactive graph editor's locked figures section.
*/
import Button from "@khanacademy/wonder-blocks-button";
import {View} from "@khanacademy/wonder-blocks-core";
import {OptionItem, SingleSelect} from "@khanacademy/wonder-blocks-dropdown";
import {TextField} from "@khanacademy/wonder-blocks-form";
Expand All @@ -13,6 +14,7 @@ import {color, spacing} from "@khanacademy/wonder-blocks-tokens";
import {LabelLarge, LabelMedium} from "@khanacademy/wonder-blocks-typography";
import copyIcon from "@phosphor-icons/core/assets/regular/copy.svg";
import autoPasteIcon from "@phosphor-icons/core/assets/regular/note-pencil.svg";
import plusCircle from "@phosphor-icons/core/regular/plus-circle.svg";
import {StyleSheet, css} from "aphrodite";
import * as React from "react";
import {useEffect, useId, useState} from "react";
Expand All @@ -24,9 +26,15 @@ import LineStrokeSelect from "./line-stroke-select";
import LineSwatch from "./line-swatch";
import LockedFigureSettingsActions from "./locked-figure-settings-actions";
import examples from "./locked-function-examples";
import LockedLabelSettings from "./locked-label-settings";
import {getDefaultFigureForType} from "./util";

import type {LockedFigureSettingsCommonProps} from "./locked-figure-settings";
import type {LockedFunctionType} from "@khanacademy/perseus";
import type {
LockedFigureColor,
LockedFunctionType,
LockedLabelType,
} from "@khanacademy/perseus";
import type {Interval} from "mafs";

export type Props = LockedFunctionType &
Expand All @@ -39,11 +47,13 @@ export type Props = LockedFunctionType &

const LockedFunctionSettings = (props: Props) => {
const {
flags,
color: lineColor,
strokeStyle,
equation,
directionalAxis,
domain,
labels,
onChangeProps,
onMove,
onRemove,
Expand Down Expand Up @@ -107,6 +117,39 @@ const LockedFunctionSettings = (props: Props) => {
? examples[exampleCategory]
: ["Select category to see example equations"];

function handleColorChange(newValue: LockedFigureColor) {
const newProps: Partial<LockedFunctionType> = {
color: newValue,
};

// Update the color of the all labels to match the point
newProps.labels = labels.map((label) => ({
...label,
color: newValue,
}));

onChangeProps(newProps);
}

function handleLabelChange(
updatedLabel: LockedLabelType,
labelIndex: number,
) {
const updatedLabels = [...labels];
updatedLabels[labelIndex] = {
...labels[labelIndex],
...updatedLabel,
};

onChangeProps({labels: updatedLabels});
}

function handleLabelRemove(labelIndex: number) {
const updatedLabels = labels.filter((_, index) => index !== labelIndex);

onChangeProps({labels: updatedLabels});
}

return (
<PerseusEditorAccordion
expanded={props.expanded}
Expand All @@ -125,9 +168,7 @@ const LockedFunctionSettings = (props: Props) => {
{/* Line color settings */}
<ColorSelect
selectedValue={lineColor}
onChange={(newValue) => {
handlePropChange("color", newValue);
}}
onChange={handleColorChange}
/>
<Strut size={spacing.small_12} />

Expand Down Expand Up @@ -246,6 +287,49 @@ const LockedFunctionSettings = (props: Props) => {
)}
</PerseusEditorAccordion>

{/* Visible Labels */}
{flags?.["mafs"]?.["locked-function-labels"] && (
<>
<View style={styles.horizontalRule} />

{labels.map((label, labelIndex) => (
<LockedLabelSettings
{...label}
expanded={true}
onChangeProps={(newLabel: LockedLabelType) => {
handleLabelChange(newLabel, labelIndex);
}}
onRemove={() => {
handleLabelRemove(labelIndex);
}}
containerStyle={styles.labelContainer}
/>
))}

<Button
kind="tertiary"
startIcon={plusCircle}
onClick={() => {
const newLabel = {
...getDefaultFigureForType("label"),
// Vertical offset for each label so they
// don't overlap.
coord: [0, -labels.length],
// Default to the same color as the function
color: lineColor,
} satisfies LockedLabelType;

onChangeProps({
labels: [...labels, newLabel],
});
}}
style={styles.addButton}
>
Add visible label
</Button>
</>
)}

{/* Actions */}
<LockedFigureSettingsActions
figureType={props.type}
Expand Down Expand Up @@ -383,6 +467,20 @@ const styles = StyleSheet.create({
textField: {
flexGrow: "1",
},

// Label settings styles
addButton: {
alignSelf: "start",
},
horizontalRule: {
marginTop: spacing.small_12,
marginBottom: spacing.xxxSmall_4,
height: 1,
backgroundColor: color.offBlack16,
},
labelContainer: {
backgroundColor: color.white,
},
});

export default LockedFunctionSettings;

0 comments on commit 8d62e1c

Please sign in to comment.