Skip to content

Commit

Permalink
[Interactive Graph Editor] UI for adding/editing/deleting locked poly…
Browse files Browse the repository at this point in the history
…gon (#1357)

## Summary:

Adding/editing/deleting Locked Polygon UI!

Issue: https://khanacademy.atlassian.net/browse/LEMS-1943

## Test plan:
[Editor page storybook](http://localhost:6006/?path=/story/perseuseditor-editorpage--mafs-with-locked-figures-m-2-flag)
[Locked Polygon Settings](http://localhost:6006/?path=/docs/perseuseditor-components-locked-polygon-settings--docs)

| 3 points, default | > 3 points, nondefault settings |
| --- | --- |
| <img width="359" alt="Screenshot 2024-06-17 at 5 07 10 PM" src="https://github.com/Khan/perseus/assets/13231763/eb3730ad-26ab-4b5f-abb1-cba662ce0f1c"> | <img width="356" alt="Screenshot 2024-06-17 at 5 07 19 PM" src="https://github.com/Khan/perseus/assets/13231763/4721b4f3-d086-4220-af3a-3a0efd282307"> |

Author: nishasy

Reviewers: benchristel, nishasy, mark-fitzgerald

Required Reviewers:

Approved By: benchristel

Checks: ✅ codecov/project, ✅ codecov/patch, ✅ Upload Coverage (ubuntu-latest, 20.x), ✅ gerald, ⏭️  Publish npm snapshot, ✅ Jest Coverage (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), ✅ Cypress (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), 🚫 Upload Coverage, ✅ gerald, ⏭️  Publish npm snapshot, 🚫 Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), 🚫 Check builds for changes in size (ubuntu-latest, 20.x), 🚫 Jest Coverage (ubuntu-latest, 20.x), 🚫 Check for .changeset entries for all changed files (ubuntu-latest, 20.x), 🚫 Cypress (ubuntu-latest, 20.x), 🚫 Upload Coverage, ✅ gerald, ⏭️  Publish npm snapshot, ✅ Cypress (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (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: #1357
  • Loading branch information
nishasy authored Jun 20, 2024
1 parent 4bb2b87 commit a608098
Show file tree
Hide file tree
Showing 15 changed files with 852 additions and 46 deletions.
6 changes: 6 additions & 0 deletions .changeset/wild-gifts-grow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@khanacademy/perseus": minor
"@khanacademy/perseus-editor": minor
---

[Interactive Graph Editor] UI for adding/editing/deleting a locked polygon
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import * as React from "react";

import LockedPolygonSettings from "../locked-polygon-settings";
import {getDefaultFigureForType} from "../util";

import type {Meta, StoryObj} from "@storybook/react";

export default {
title: "PerseusEditor/Components/Locked Polygon Settings",
component: LockedPolygonSettings,
} as Meta<typeof LockedPolygonSettings>;

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

type StoryComponentType = StoryObj<typeof LockedPolygonSettings>;

// Set the default values in the control panel.
Default.args = {
...getDefaultFigureForType("polygon"),
onChangeProps: () => {},
onRemove: () => {},
};

export const Controlled: StoryComponentType = {
render: function Render() {
const [props, setProps] = React.useState({
...getDefaultFigureForType("polygon"),
onRemove: () => {},
});

const handlePropsUpdate = (newProps) => {
setProps({
...props,
...newProps,
});
};

return (
<LockedPolygonSettings
{...props}
onChangeProps={handlePropsUpdate}
/>
);
},
};

Controlled.parameters = {
chromatic: {
// Disabling because this is testing behavior, not visuals.
disableSnapshot: true,
},
};

// Fully expanded view of the locked ellipse settings to allow snapshot testing.
export const Expanded: StoryComponentType = {
render: function Render() {
const [expanded, setExpanded] = React.useState(true);
const [props, setProps] = React.useState({
...getDefaultFigureForType("polygon"),
onRemove: () => {},
});

const handlePropsUpdate = (newProps) => {
setProps({
...props,
...newProps,
});
};

return (
<LockedPolygonSettings
{...props}
expanded={expanded}
onToggle={setExpanded}
onChangeProps={handlePropsUpdate}
/>
);
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
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 LockedPolygonSettings from "../locked-polygon-settings";
import {getDefaultFigureForType} from "../util";

import type {UserEvent} from "@testing-library/user-event";

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

describe("LockedPolygonSettings", () => {
let userEvent: UserEvent;
beforeEach(() => {
userEvent = userEventLib.setup({
advanceTimers: jest.advanceTimersByTime,
});
});
test("renders", () => {
// Arrange

// Act
render(<LockedPolygonSettings {...defaultProps} />, {
wrapper: RenderStateRoot,
});

// Assert
const titleText = screen.getByText("Polygon, 3 sides");
expect(titleText).toBeInTheDocument();
});

test("summary reflects number of sides", () => {
// Arrange

// Act
render(
<LockedPolygonSettings
{...defaultProps}
points={[
[0, 0],
[1, 1],
[2, 2],
[1, -1],
]}
/>,
{
wrapper: RenderStateRoot,
},
);

// Assert
const titleText = screen.getByText("Polygon, 4 sides");
expect(titleText).toBeInTheDocument();
});

test("summary reflects color", () => {
// Arrange

// Act
render(<LockedPolygonSettings {...defaultProps} color="blue" />, {
wrapper: RenderStateRoot,
});

// Assert
const titleText = screen.getByLabelText(
"blue, stroke solid, fill none",
);
expect(titleText).toBeInTheDocument();
});

test("summary reflects stroke style", () => {
// Arrange

// Act
render(
<LockedPolygonSettings {...defaultProps} strokeStyle="dashed" />,
{
wrapper: RenderStateRoot,
},
);

// Assert
const titleText = screen.getByLabelText(
"grayH, stroke dashed, fill none",
);
expect(titleText).toBeInTheDocument();
});

test("summary reflects fill style", () => {
// Arrange

// Act
render(<LockedPolygonSettings {...defaultProps} fillStyle="solid" />, {
wrapper: RenderStateRoot,
});

// Assert
const titleText = screen.getByLabelText(
"grayH, stroke solid, fill solid",
);
expect(titleText).toBeInTheDocument();
});

test("shows delete buttons when there are more than 3 points", () => {
// Arrange

// Act
render(
<LockedPolygonSettings
{...defaultProps}
points={[
[0, 0],
[1, 1],
[2, 2],
[1, -1],
]}
/>,
{
wrapper: RenderStateRoot,
},
);

// Assert
const deleteButtons = screen.getAllByLabelText(/Delete polygon point/);
expect(deleteButtons).toHaveLength(4);
});

test("does not show delete buttons when there are 3 points", () => {
// Arrange

// Act
render(<LockedPolygonSettings {...defaultProps} />, {
wrapper: RenderStateRoot,
});

// Assert
const deleteButtons =
screen.queryAllByLabelText(/Delete polygon point/);
expect(deleteButtons).toHaveLength(0);
});

test("calls onToggle when header is clicked", async () => {
// Arrange
const onToggle = jest.fn();
render(
<LockedPolygonSettings {...defaultProps} onToggle={onToggle} />,
{
wrapper: RenderStateRoot,
},
);

// Act
const header = screen.getByRole("button", {
name: "Polygon, 3 sides grayH, stroke solid, fill none",
});
await userEvent.click(header);

// Assert
expect(onToggle).toHaveBeenCalled();
});
});
65 changes: 31 additions & 34 deletions packages/perseus-editor/src/components/coordinate-pair-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ import {StyleSheet} from "aphrodite";
import * as React from "react";

import type {Coord} from "@khanacademy/perseus";
import type {StyleType} from "@khanacademy/wonder-blocks-core";

type Props = {
coord: [number, number];
labels?: [string, string];
error?: boolean;
style?: StyleType;
onChange: (newCoord: Coord) => void;
};

const CoordinatePairInput = (props: Props) => {
const {coord, labels, error, onChange} = props;
const {coord, labels, error, style, onChange} = props;

// Keep track of the coordinates via state as the user is editing them,
// before they are updated in the props as a valid number.
Expand Down Expand Up @@ -45,39 +47,37 @@ const CoordinatePairInput = (props: Props) => {
}

return (
<View>
<View style={[styles.row, styles.spaceUnder]}>
<LabelMedium tag="label" style={styles.row}>
{labels ? labels[0] : "x coord"}
<View style={[styles.row, style]}>
<LabelMedium tag="label" style={styles.row}>
{labels ? labels[0] : "x coord"}

<Strut size={spacing.xxSmall_6} />
<TextField
type="number"
value={coordState[0]}
onChange={(newValue) => handleCoordChange(newValue, 0)}
style={[
styles.textField,
error ? styles.errorField : undefined,
]}
/>
</LabelMedium>
<Strut size={spacing.medium_16} />
<Strut size={spacing.xxSmall_6} />
<TextField
type="number"
value={coordState[0]}
onChange={(newValue) => handleCoordChange(newValue, 0)}
style={[
styles.textField,
error ? styles.errorField : undefined,
]}
/>
</LabelMedium>
<Strut size={spacing.medium_16} />

<LabelMedium tag="label" style={styles.row}>
{labels ? labels[1] : "y coord"}
<LabelMedium tag="label" style={styles.row}>
{labels ? labels[1] : "y coord"}

<Strut size={spacing.xxSmall_6} />
<TextField
type="number"
value={coordState[1]}
onChange={(newValue) => handleCoordChange(newValue, 1)}
style={[
styles.textField,
error ? styles.errorField : undefined,
]}
/>
</LabelMedium>
</View>
<Strut size={spacing.xxSmall_6} />
<TextField
type="number"
value={coordState[1]}
onChange={(newValue) => handleCoordChange(newValue, 1)}
style={[
styles.textField,
error ? styles.errorField : undefined,
]}
/>
</LabelMedium>
</View>
);
};
Expand All @@ -88,9 +88,6 @@ const styles = StyleSheet.create({
flexDirection: "row",
alignItems: "center",
},
spaceUnder: {
marginBottom: spacing.xSmall_8,
},
textField: {
width: spacing.xxxLarge_64,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ const DefiningPointSettings = (props: Props) => {
<CoordinatePairInput
coord={coord}
error={!!error}
style={styles.spaceUnder}
onChange={(newCoords) => {
onChangeProps({coord: newCoords});
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const LockedEllipseSettings = (props: Props) => {
<View style={styles.row}>
<CoordinatePairInput
coord={center}
style={styles.spaceUnder}
onChange={(newCoords: Coord) =>
onChangeProps({center: newCoords})
}
Expand All @@ -90,6 +91,7 @@ const LockedEllipseSettings = (props: Props) => {
<CoordinatePairInput
coord={radius}
labels={["x radius", "y radius"]}
style={styles.spaceUnder}
onChange={(newCoords: Coord) =>
onChangeProps({radius: newCoords})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const LockedFigureSelect = (props: Props) => {
const {id, onChange} = props;

const figureTypes = props.showM2Features
? ["point", "line", "ellipse", "vector"]
? ["point", "line", "vector", "ellipse", "polygon"]
: ["point", "line"];

return (
Expand Down
Loading

0 comments on commit a608098

Please sign in to comment.