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

[Locked Labels] Implement adding/editing/deleting a standalone locked label #1539

Merged
merged 9 commits into from
Aug 22, 2024
6 changes: 6 additions & 0 deletions .changeset/angry-hounds-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@khanacademy/perseus": patch
"@khanacademy/perseus-editor": minor
---

[Locked Labels] Implement adding/editing/deleting a standalone locked label
Original file line number Diff line number Diff line change
@@ -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<typeof LockedLabelSettings>;

export const Default = (args): React.ReactElement => {
return <LockedLabelSettings {...args} />;
};

const defaultProps = {
...getDefaultFigureForType("label"),
onChangeProps: () => {},
onMove: () => {},
onRemove: () => {},
};

type StoryComponentType = StoryObj<typeof LockedLabelSettings>;

// 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 (
<LockedLabelSettings
{...props}
expanded={true}
onChangeProps={handlePropsUpdate}
/>
);
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -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(<LockedFunctionSettings {...defaultProps} />, {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
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(<LockedLabelSettings {...defaultProps} />, {
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(
<LockedLabelSettings
{...defaultProps}
coord={[1, 2]}
text=""
/>,
{
wrapper: RenderStateRoot,
},
);

// Act
const titleText = screen.getByText("Label (1, 2)");

// Assert
expect(titleText).toBeInTheDocument();
});

test("should show the text in the header", () => {
// Arrange
render(
<LockedLabelSettings
{...defaultProps}
color="blue"
text="something"
/>,
{
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(
<LockedLabelSettings
{...defaultProps}
text="something"
onToggle={onToggle}
/>,
{
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(
<LockedLabelSettings
{...defaultProps}
onChangeProps={onChangeProps}
/>,
{
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.mock.calls).toEqual([
[{coord: [1, 0]}],
[{coord: [0, 2]}],
]);
});

test("calls 'onChangeProps' when text is changed", async () => {
// Arrange
render(
<LockedLabelSettings
{...defaultProps}
onChangeProps={onChangeProps}
/>,
{
wrapper: RenderStateRoot,
},
);

// Act
const textInput = screen.getByRole("textbox", {
name: "TeX",
});
await userEvent.type(textInput, "x^2");

// 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.mock.calls).toEqual([
[{text: "x"}],
[{text: "^"}],
[{text: "2"}],
]);
});

test("calls 'onChangeProps' when color is changed", async () => {
// Arrange
render(
<LockedLabelSettings
{...defaultProps}
onChangeProps={onChangeProps}
/>,
{
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(
<LockedLabelSettings
{...defaultProps}
onChangeProps={onChangeProps}
/>,
{
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"});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand All @@ -22,17 +26,20 @@ 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 (
<View style={styles.container}>
<ActionMenu
menuText="Add locked figure"
style={styles.addElementSelect}
>
{figureTypesCurrent.map((figureType) => (
{figureTypes.map((figureType) => (
<ActionItem
key={`${id}-${figureType}`}
label={figureType}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as React from "react";

import LockedEllipseSettings from "./locked-ellipse-settings";
import LockedFunctionSettings from "./locked-function-settings";
import LockedLabelSettings from "./locked-label-settings";
import LockedLineSettings from "./locked-line-settings";
import LockedPointSettings from "./locked-point-settings";
import LockedPolygonSettings from "./locked-polygon-settings";
Expand All @@ -17,6 +18,7 @@ import LockedVectorSettings from "./locked-vector-settings";
import type {Props as LockedEllipseProps} from "./locked-ellipse-settings";
import type {LockedFigureSettingsMovementType} from "./locked-figure-settings-actions";
import type {Props as LockedFunctionProps} from "./locked-function-settings";
import type {Props as LockedLabelProps} from "./locked-label-settings";
import type {Props as LockedLineProps} from "./locked-line-settings";
import type {Props as LockedPointProps} from "./locked-point-settings";
import type {Props as LockedPolygonProps} from "./locked-polygon-settings";
Expand All @@ -26,6 +28,10 @@ export type LockedFigureSettingsCommonProps = {
// 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;

// Movement props
/**
Expand Down Expand Up @@ -57,6 +63,7 @@ type Props = LockedFigureSettingsCommonProps &
| LockedVectorProps
| LockedPolygonProps
| LockedFunctionProps
| LockedLabelProps
);

const LockedFigureSettings = (props: Props) => {
Expand All @@ -76,6 +83,11 @@ const LockedFigureSettings = (props: Props) => {
return <LockedFunctionSettings {...props} />;
}
break;
case "label":
if (props.showLabelsFlag) {
return <LockedLabelSettings {...props} />;
}
break;
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<LockedFigure>;
onChange: (props: Partial<InteractiveGraphEditorProps>) => void;
};
Expand Down Expand Up @@ -170,6 +174,7 @@ const LockedFiguresSection = (props: Props) => {
<LockedFigureSettings
key={`${uniqueId}-locked-${figure}-${index}`}
showM2bFeatures={props.showM2bFeatures}
showLabelsFlag={props.showLabelsFlag}
expanded={expandedStates[index]}
onToggle={(newValue) => {
const newExpanded = [...expandedStates];
Expand All @@ -190,6 +195,7 @@ const LockedFiguresSection = (props: Props) => {
<View style={styles.buttonContainer}>
<LockedFigureSelect
showM2bFeatures={props.showM2bFeatures}
showLabelsFlag={props.showLabelsFlag}
id={`${uniqueId}-select`}
onChange={addLockedFigure}
/>
Expand Down
Loading