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 Editor] Add UI for updating starting coordinates (linear and ray) #1382

Merged
merged 19 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/chilly-planets-walk.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] Implement UI to edit start coordinates for linear and ray graphs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export const MafsWithLockedFiguresCurrent = (): React.ReactElement => {
flags: {
mafs: {
...flags.mafs,
"start-coords-ui": false,
"interactive-graph-locked-features-m2": false,
"interactive-graph-locked-features-m2b": false,
},
Expand All @@ -152,6 +153,7 @@ export const MafsWithLockedFiguresM2Flag = (): React.ReactElement => {
flags: {
mafs: {
...flags.mafs,
"start-coords-ui": false,
"interactive-graph-locked-features-m2": true,
"interactive-graph-locked-features-m2b": false,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const flags = {
linear: true,
"linear-system": true,
ray: true,
"start-coords-ui": true,
"interactive-graph-locked-features-m2": true,
"interactive-graph-locked-features-m2b": true,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import {Dependencies} from "@khanacademy/perseus";
import {render, screen} from "@testing-library/react";
import {userEvent as userEventLib} from "@testing-library/user-event";
import * as React from "react";

import {testDependencies} from "../../../../../testing/test-dependencies";
import StartCoordSettings from "../start-coord-settings";

import type {Range} from "@khanacademy/perseus";

const defaultProps = {
range: [
[-10, 10],
[-10, 10],
] satisfies [Range, Range],
step: [1, 1] satisfies [number, number],
};
describe("StartCoordSettings", () => {
let userEvent;
beforeEach(() => {
userEvent = userEventLib.setup({
advanceTimers: jest.advanceTimersByTime,
});

jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
testDependencies,
);
});

test("clicking the heading toggles the settings", async () => {
// Arrange

// Act
render(
<StartCoordSettings
{...defaultProps}
type="linear"
onChange={() => {}}
/>,
);

const heading = screen.getByText("Start coordinates");

// Assert
expect(screen.getByText("Point 1")).toBeInTheDocument();
expect(screen.getByText("Point 2")).toBeInTheDocument();

await userEvent.click(heading);

expect(screen.queryByText("Point 1")).not.toBeInTheDocument();
expect(screen.queryByText("Point 2")).not.toBeInTheDocument();

await userEvent.click(heading);

expect(screen.getByText("Point 1")).toBeInTheDocument();
expect(screen.getByText("Point 2")).toBeInTheDocument();
});

describe.each`
type
${"linear"}
${"ray"}
`(`graphs with CollinearTuple coords`, ({type}) => {
test(`shows the start coordinates UI for ${type}`, () => {
// Arrange

// Act
render(
<StartCoordSettings
{...defaultProps}
type={type}
onChange={() => {}}
/>,
);

const resetButton = screen.getByRole("button", {
name: "Use default start coords",
});

// Assert
expect(screen.getByText("Start coordinates")).toBeInTheDocument();
expect(screen.getByText("Point 1")).toBeInTheDocument();
expect(screen.getByText("Point 2")).toBeInTheDocument();
expect(resetButton).toBeInTheDocument();
});

test.each`
segmentIndex | coord
${0} | ${"x"}
${0} | ${"y"}
${1} | ${"x"}
${1} | ${"y"}
`(
`calls onChange when $coord coord is changed (segment $segmentIndex) for type ${type}`,
async ({segmentIndex, coord}) => {
// Arrange
const onChangeMock = jest.fn();

// Act
render(
<StartCoordSettings
{...defaultProps}
type={type}
onChange={onChangeMock}
/>,
);

// Assert
const input = screen.getAllByRole("spinbutton", {
name: `${coord} coord`,
})[segmentIndex];
await userEvent.clear(input);
await userEvent.type(input, "101");

const expectedCoords = [
[-5, 5],
[5, 5],
];
expectedCoords[segmentIndex][coord === "x" ? 0 : 1] = 101;

expect(onChangeMock).toHaveBeenLastCalledWith(expectedCoords);
},
);

test(`calls onChange when reset button is clicked for type ${type}`, async () => {
// Arrange
const onChangeMock = jest.fn();

// Act

nishasy marked this conversation as resolved.
Show resolved Hide resolved
render(
<StartCoordSettings
{...defaultProps}
coords={[
[-15, 15],
[15, 15],
]}
type={type}
onChange={onChangeMock}
/>,
);

// Assert
const resetButton = screen.getByRole("button", {
name: "Use default start coords",
});
await userEvent.click(resetButton);

expect(onChangeMock).toHaveBeenLastCalledWith([
[-5, 5],
[5, 5],
]);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ const CoordinatePairInput = (props: Props) => {
coord[1].toString(),
]);

// Update the local state when the props change. (Such as when the graph
// type is changed, and the coordinates are reset.)
React.useEffect(() => {
setCoordState([coord[0].toString(), coord[1].toString()]);
}, [coord]);

function handleCoordChange(newValue, coordIndex) {
// Update the local state (update the input field value).
const newCoordState = [...coordState];
Expand Down
117 changes: 117 additions & 0 deletions packages/perseus-editor/src/components/start-coord-settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import {getLineCoords} from "@khanacademy/perseus";
import Button from "@khanacademy/wonder-blocks-button";
import {View} from "@khanacademy/wonder-blocks-core";
import {Strut} from "@khanacademy/wonder-blocks-layout";
import {color, spacing} from "@khanacademy/wonder-blocks-tokens";
import {LabelLarge} from "@khanacademy/wonder-blocks-typography";
import arrowCounterClockwise from "@phosphor-icons/core/bold/arrow-counter-clockwise-bold.svg";
import {StyleSheet} from "aphrodite";
import * as React from "react";

import CoordinatePairInput from "./coordinate-pair-input";
import Heading from "./heading";

import type {
PerseusGraphType,
Range,
CollinearTuple,
} from "@khanacademy/perseus";

type Props = PerseusGraphType & {
range: [x: Range, y: Range];
step: [x: number, y: number];
onChange: (coords: CollinearTuple) => void;
};

type PropsInner = {
type: PerseusGraphType["type"];
coords: CollinearTuple;
onChange: (coords: CollinearTuple) => void;
};

const StartCoordSettingsInner = (props: PropsInner) => {
const {type, coords, onChange} = props;

// Check if coords is of type CollinearTuple
switch (type) {
case "linear":
case "ray":
return (
<>
<View style={styles.tile}>
<LabelLarge>Point 1</LabelLarge>
<CoordinatePairInput
coord={coords[0]}
onChange={(value) => onChange([value, coords[1]])}
/>
</View>
<View style={styles.tile}>
<LabelLarge>Point 2</LabelLarge>
<CoordinatePairInput
coord={coords[1]}
onChange={(value) => onChange([coords[0], value])}
/>
</View>
</>
);
default:
return null;
}
};

const StartCoordSettings = (props: Props) => {
const {type, range, step, onChange} = props;
const [isOpen, setIsOpen] = React.useState(true);

if (type !== "linear" && type !== "ray") {
return null;
}

const defaultStartCoords = getLineCoords({type: type}, range, step);

return (
<View style={styles.container}>
{/* Heading for the collapsible section */}
<Heading
title="Start coordinates"
isOpen={isOpen}
onToggle={() => setIsOpen(!isOpen)}
/>

{/* Start coordinates main UI */}
{isOpen && (
<>
<StartCoordSettingsInner
type={type}
coords={props.coords ?? defaultStartCoords}
onChange={onChange}
/>

{/* Button to reset to default */}
<Strut size={spacing.small_12} />
<Button
startIcon={arrowCounterClockwise}
kind="tertiary"
size="small"
onClick={() => {
onChange(defaultStartCoords);
}}
>
Use default start coords
</Button>
</>
)}
</View>
);
};

const styles = StyleSheet.create({
tile: {
backgroundColor: color.fadedBlue8,
marginTop: spacing.xSmall_8,
padding: spacing.small_12,
borderRadius: spacing.xSmall_8,
},
});

export default StartCoordSettings;
Loading
Loading