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

[Interactive Graph] View a locked polygon #1353

Merged
merged 8 commits into from
Jun 17, 2024
6 changes: 6 additions & 0 deletions .changeset/chatty-pears-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@khanacademy/perseus": patch
"@khanacademy/perseus-editor": patch
---

[Interactive Graph Editor] Add locked vector to storybook story for all locked figures
6 changes: 6 additions & 0 deletions .changeset/healthy-peas-sin.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] Update the locked ellipse settings so they only take degrees as input.
6 changes: 6 additions & 0 deletions .changeset/metal-apricots-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@khanacademy/perseus": minor
"@khanacademy/perseus-editor": minor
---

[Interactive Graph] View a locked polygon
6 changes: 6 additions & 0 deletions .changeset/sweet-jokes-poke.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] Add blue and gold to locked figures colorset
Original file line number Diff line number Diff line change
Expand Up @@ -15,91 +15,52 @@ describe("AngleInput", () => {
});
});

test("calls onChange with new angle (radians)", async () => {
test("displays angle in degrees", () => {
// Arrange
const onChangeProps = jest.fn();
render(<AngleInput angle={0} onChange={onChangeProps} />, {
render(<AngleInput angle={Math.PI / 4} onChange={() => {}} />, {
wrapper: RenderStateRoot,
});

// Act
const angleInput = screen.getByLabelText("angle");
await userEvent.type(angleInput, "2pi");

// Assert
expect(onChangeProps).toHaveBeenLastCalledWith(2 * Math.PI);
// Called with "2" and "2pi", not with "2p".
expect(onChangeProps).toHaveBeenCalledTimes(2);
});

test("calls onChange with new angle (degrees)", async () => {
// Arrange
const onChangeProps = jest.fn();
render(<AngleInput angle={0} onChange={onChangeProps} />, {
wrapper: RenderStateRoot,
const angleInput = screen.getByRole("spinbutton", {
name: "angle (degrees)",
});

// Act
const angleSwitch = screen.getByRole("switch");
await userEvent.click(angleSwitch);
const angleInput = screen.getByLabelText("angle");
await userEvent.type(angleInput, "180");

// Assert
expect(onChangeProps).toHaveBeenLastCalledWith(Math.PI);
// Called with the switch, then with "1", "18", and "180".
expect(onChangeProps).toHaveBeenCalledTimes(4);
expect(angleInput).toHaveValue(45);
});

test("does not call onChange with invalid expression", async () => {
test("calls onChange with new angle in radians", async () => {
// Arrange
const onChangeProps = jest.fn();
render(<AngleInput angle={0} onChange={onChangeProps} />, {
wrapper: RenderStateRoot,
});

// Act
const angleInput = screen.getByLabelText("angle");
await userEvent.type(angleInput, "2pi +");

// Assert
// Called with "2", "2pi", and "2pi ", but
// not with "2p" or "2pi +".
expect(onChangeProps).toHaveBeenCalledTimes(3);
expect(onChangeProps).toHaveBeenLastCalledWith(2 * Math.PI);
});

test("calls onChange in radians when switched to degrees", async () => {
// Arrange
const onChangeProps = jest.fn();
render(<AngleInput angle={0} onChange={onChangeProps} />, {
wrapper: RenderStateRoot,
const angleInput = screen.getByRole("spinbutton", {
name: "angle (degrees)",
});

// Act
const angleInput = screen.getByLabelText("angle");
await userEvent.type(angleInput, "180");
const angleSwitch = screen.getByRole("switch");
await userEvent.click(angleSwitch);
await userEvent.type(angleInput, "90");

// Assert
expect(onChangeProps).toHaveBeenLastCalledWith(Math.PI);
expect(onChangeProps).toHaveBeenLastCalledWith(Math.PI / 2);
});

test("calls onChange in radians when switched to from degrees to radians", async () => {
test("does not call onChange with invalid expression", async () => {
// Arrange
const onChangeProps = jest.fn();
render(<AngleInput angle={0} onChange={onChangeProps} />, {
wrapper: RenderStateRoot,
});

// Act
const angleInput = screen.getByLabelText("angle");
await userEvent.type(angleInput, "360");
const angleSwitch = screen.getByRole("switch");
await userEvent.click(angleSwitch);
const angleInput = screen.getByRole("spinbutton", {
name: "angle (degrees)",
});
await userEvent.type(angleInput, "-");

// Assert
expect(onChangeProps).toHaveBeenLastCalledWith(2 * Math.PI);
expect(onChangeProps).not.toHaveBeenCalled();
});
});
26 changes: 21 additions & 5 deletions packages/perseus-editor/src/components/__tests__/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ describe("getDefaultFigureForType", () => {
});
});

test("should return a vector with default values", () => {
const figure = getDefaultFigureForType("vector");
expect(figure).toEqual({
type: "vector",
points: [
[0, 0],
[2, 2],
],
color: "grayH",
});
});

test("should return an ellipse with default values", () => {
const figure = getDefaultFigureForType("ellipse");
expect(figure).toEqual({
Expand All @@ -50,15 +62,19 @@ describe("getDefaultFigureForType", () => {
});
});

test("should return a vector with default values", () => {
const figure = getDefaultFigureForType("vector");
test("should return a polygon with default values", () => {
const figure = getDefaultFigureForType("polygon");
expect(figure).toEqual({
type: "vector",
type: "polygon",
points: [
[0, 0],
[2, 2],
[0, 2],
[-1, 0],
[1, 0],
],
color: "grayH",
showVertices: false,
fillStyle: "none",
strokeStyle: "solid",
});
});
});
92 changes: 24 additions & 68 deletions packages/perseus-editor/src/components/angle-input.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import * as KAS from "@khanacademy/kas";
import {components} from "@khanacademy/perseus";
import {View} from "@khanacademy/wonder-blocks-core";
import {TextField} from "@khanacademy/wonder-blocks-form";
import {Strut} from "@khanacademy/wonder-blocks-layout";
import Switch from "@khanacademy/wonder-blocks-switch";
import {spacing} from "@khanacademy/wonder-blocks-tokens";
import {LabelMedium, LabelSmall} from "@khanacademy/wonder-blocks-typography";
import {LabelMedium} from "@khanacademy/wonder-blocks-typography";
import {StyleSheet} from "aphrodite";
import * as React from "react";

const {InfoTip} = components;

const degreeToRadian = (degrees: number) => {
return (degrees / 180) * Math.PI;
};
const radianToDegree = (radians: number) => {
return (radians / Math.PI) * 180;
};

type Props = {
angle: number;
Expand All @@ -23,73 +20,36 @@ type Props = {
const AngleInput = (props: Props) => {
const {angle, onChange} = props;

const [angleInput, setAngleInput] = React.useState(angle.toString());
const [isInDegrees, setIsInDegrees] = React.useState(false);
const [angleInput, setAngleInput] = React.useState(
radianToDegree(angle).toString(),
);

function handleAngleChange(newValue, useDegrees = isInDegrees) {
function handleAngleChange(newValue) {
// Update the local state (update the input field value).
setAngleInput(newValue);

try {
// If the new value is a valid expression, update the props.
// Save the angle in radians.
const evaluatedAngle = KAS.parse(newValue).expr.eval();

if (useDegrees) {
onChange(degreeToRadian(evaluatedAngle));
} else {
onChange(evaluatedAngle);
}
} catch (e) {
// The user likely has not finished typing the expression.
// Do nothing.
// If the new value is not a number, don't update the props.
// If it's empty, keep the props the same value instead of setting to 0.
if (isNaN(+newValue) || newValue === "") {
return;
}
}

function handleAngleTypeChange() {
// Change the angle based on the new angle type.
handleAngleChange(angleInput, !isInDegrees);

// Update the angle to the new type.
setIsInDegrees((usingDegrees) => !usingDegrees);
// Update the graph.
onChange(degreeToRadian(newValue));
}

return (
<View style={[styles.row, styles.spaceUnder]}>
{/* Label */}
<LabelMedium tag="label" style={styles.row}>
angle
<Strut size={spacing.xxSmall_6} />
<TextField
value={angleInput}
onChange={handleAngleChange}
style={styles.textField}
/>
</LabelMedium>

{/* Spacing */}
<LabelMedium tag="label" style={styles.row}>
angle (degrees)
<Strut size={spacing.xxSmall_6} />

{/* Radian/Degree Toggle */}
<LabelSmall>radians</LabelSmall>
<View style={styles.switch}>
<Switch
onChange={handleAngleTypeChange}
checked={isInDegrees}
/>
</View>
<LabelSmall>degrees</LabelSmall>

{/* Info Tooltip */}
<InfoTip>
<p>
The angle of rotation for the ellipse (if the x radius and y
radius are different).
</p>
<p>Expressions will be evaluted (e.g. "pi/2" or "5pi/4").</p>
</InfoTip>
</View>
<TextField
type="number"
value={angleInput}
onChange={handleAngleChange}
style={styles.textField}
/>
<Strut size={spacing.xxSmall_6} />
</LabelMedium>
);
};

Expand All @@ -99,12 +59,8 @@ const styles = StyleSheet.create({
flexDirection: "row",
alignItems: "center",
},
switch: {
marginLeft: spacing.xxSmall_6,
marginRight: spacing.xxSmall_6,
},
textField: {
maxWidth: spacing.xxxLarge_64,
width: spacing.xxxLarge_64,
},
});

Expand Down
11 changes: 4 additions & 7 deletions packages/perseus-editor/src/components/ellipse-swatch.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
import {
lockedFigureColors,
lockedEllipseFillStyles,
} from "@khanacademy/perseus";
import {lockedFigureColors, lockedFigureFillStyles} from "@khanacademy/perseus";
import {View} from "@khanacademy/wonder-blocks-core";
import {color as wbColor, spacing} from "@khanacademy/wonder-blocks-tokens";
import {StyleSheet} from "aphrodite";
import * as React from "react";

import type {
LockedFigureColor,
LockedEllipseFillType,
LockedFigureFillType,
} from "@khanacademy/perseus";

type Props = {
color: LockedFigureColor;
fillStyle: LockedEllipseFillType;
fillStyle: LockedFigureFillType;
strokeStyle: "solid" | "dashed";
};

Expand All @@ -36,7 +33,7 @@ const EllipseSwatch = (props: Props) => {
styles.innerCircle,
{
backgroundColor: lockedFigureColors[color],
opacity: lockedEllipseFillStyles[fillStyle],
opacity: lockedFigureFillStyles[fillStyle],
},
]}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {components, lockedEllipseFillStyles} from "@khanacademy/perseus";
import {components, lockedFigureFillStyles} from "@khanacademy/perseus";
import {View} from "@khanacademy/wonder-blocks-core";
import {OptionItem, SingleSelect} from "@khanacademy/wonder-blocks-dropdown";
import {Strut} from "@khanacademy/wonder-blocks-layout";
Expand All @@ -17,7 +17,7 @@ import LockedFigureSettingsActions from "./locked-figure-settings-actions";
import type {AccordionProps} from "./locked-figure-settings";
import type {
Coord,
LockedEllipseFillType,
LockedFigureFillType,
LockedEllipseType,
LockedFigureColor,
} from "@khanacademy/perseus";
Expand Down Expand Up @@ -118,13 +118,13 @@ const LockedEllipseSettings = (props: Props) => {
<Strut size={spacing.xxSmall_6} />
<SingleSelect
selectedValue={fillStyle}
onChange={(value: LockedEllipseFillType) =>
onChange={(value: LockedFigureFillType) =>
onChangeProps({fillStyle: value})
}
// Placeholder is required, but never gets used.
placeholder=""
>
{Object.keys(lockedEllipseFillStyles).map((option) => (
{Object.keys(lockedFigureFillStyles).map((option) => (
<OptionItem
key={option}
value={option}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@
return (
<View>
{figures?.map((figure, index) => {
if (figure.type === "polygon") {
// TODO(LEMS-1943): Implement locked polygon settings.
// Remove this block once locked polygon settings are
// implemented.
return;
}

Check warning on line 104 in packages/perseus-editor/src/components/locked-figures-section.tsx

View check run for this annotation

Codecov / codecov/patch

packages/perseus-editor/src/components/locked-figures-section.tsx#L100-L104

Added lines #L100 - L104 were not covered by tests

return (
<LockedFigureSettings
key={`${uniqueId}-locked-${figure}-${index}`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ const LockedLineSettings = (props: Props) => {

const styles = StyleSheet.create({
row: {
display: "flex",
flexDirection: "row",
alignItems: "center",
},
Expand Down
Loading
Loading