diff --git a/.changeset/neat-carrots-guess.md b/.changeset/neat-carrots-guess.md
new file mode 100644
index 0000000000..375bbbed7b
--- /dev/null
+++ b/.changeset/neat-carrots-guess.md
@@ -0,0 +1,6 @@
+---
+"@khanacademy/perseus": minor
+"@khanacademy/perseus-editor": minor
+---
+
+[Interactive Graph Editor] Add the ability to reorder locked figure settings
diff --git a/packages/perseus-editor/src/components/__stories__/locked-ellipse-settings.stories.tsx b/packages/perseus-editor/src/components/__stories__/locked-ellipse-settings.stories.tsx
index 7945f9313e..ade3409a31 100644
--- a/packages/perseus-editor/src/components/__stories__/locked-ellipse-settings.stories.tsx
+++ b/packages/perseus-editor/src/components/__stories__/locked-ellipse-settings.stories.tsx
@@ -14,21 +14,21 @@ export const Default = (args): React.ReactElement => {
return ;
};
-type StoryComponentType = StoryObj;
-
-// Set the default values in the control panel.
-Default.args = {
+const defaultProps = {
...getDefaultFigureForType("ellipse"),
onChangeProps: () => {},
+ onMove: () => {},
onRemove: () => {},
};
+type StoryComponentType = StoryObj;
+
+// Set the default values in the control panel.
+Default.args = defaultProps;
+
export const Controlled: StoryComponentType = {
render: function Render() {
- const [props, setProps] = React.useState({
- ...getDefaultFigureForType("ellipse"),
- onRemove: () => {},
- });
+ const [props, setProps] = React.useState(defaultProps);
const handlePropsUpdate = (newProps) => {
setProps({
@@ -57,10 +57,7 @@ Controlled.parameters = {
export const Expanded: StoryComponentType = {
render: function Render() {
const [expanded, setExpanded] = React.useState(true);
- const [props, setProps] = React.useState({
- ...getDefaultFigureForType("ellipse"),
- onRemove: () => {},
- });
+ const [props, setProps] = React.useState(defaultProps);
const handlePropsUpdate = (newProps) => {
setProps({
diff --git a/packages/perseus-editor/src/components/__stories__/locked-line-settings.stories.tsx b/packages/perseus-editor/src/components/__stories__/locked-line-settings.stories.tsx
index 8c053f8158..50c762d67a 100644
--- a/packages/perseus-editor/src/components/__stories__/locked-line-settings.stories.tsx
+++ b/packages/perseus-editor/src/components/__stories__/locked-line-settings.stories.tsx
@@ -14,21 +14,21 @@ export const Default = (args): React.ReactElement => {
return ;
};
-type StoryComponentType = StoryObj;
-
-// Set the default values in the control panel.
-Default.args = {
+const defaultProps = {
...getDefaultFigureForType("line"),
onChangeProps: () => {},
+ onMove: () => {},
onRemove: () => {},
};
+type StoryComponentType = StoryObj;
+
+// Set the default values in the control panel.
+Default.args = defaultProps;
+
export const Controlled: StoryComponentType = {
render: function Render() {
- const [props, setProps] = React.useState({
- ...getDefaultFigureForType("line"),
- onRemove: () => {},
- });
+ const [props, setProps] = React.useState(defaultProps);
const handlePropsUpdate = (newProps) => {
setProps({
@@ -57,10 +57,7 @@ Controlled.parameters = {
*/
export const WithInvalidPoints: StoryComponentType = {
render: function Render() {
- const [props, setProps] = React.useState({
- ...getDefaultFigureForType("line"),
- onRemove: () => {},
- });
+ const [props, setProps] = React.useState(defaultProps);
const handlePropsUpdate = (newProps) => {
setProps({
@@ -87,10 +84,7 @@ export const WithInvalidPoints: StoryComponentType = {
export const Expanded: StoryComponentType = {
render: function Render() {
const [expanded, setExpanded] = React.useState(true);
- const [props, setProps] = React.useState({
- ...getDefaultFigureForType("line"),
- onRemove: () => {},
- });
+ const [props, setProps] = React.useState(defaultProps);
const handlePropsUpdate = (newProps) => {
setProps({
@@ -115,8 +109,7 @@ export const ExpandedNondefaultProps: StoryComponentType = {
render: function Render() {
const [expanded, setExpanded] = React.useState(true);
const [props, setProps] = React.useState({
- ...getDefaultFigureForType("line"),
- onRemove: () => {},
+ ...defaultProps,
kind: "segment" as const,
color: "green" as const,
lineStyle: "dashed" as const,
diff --git a/packages/perseus-editor/src/components/__stories__/locked-point-settings.stories.tsx b/packages/perseus-editor/src/components/__stories__/locked-point-settings.stories.tsx
index a500d36bb6..1b45eeff3f 100644
--- a/packages/perseus-editor/src/components/__stories__/locked-point-settings.stories.tsx
+++ b/packages/perseus-editor/src/components/__stories__/locked-point-settings.stories.tsx
@@ -14,21 +14,21 @@ export const Default = (args): React.ReactElement => {
return ;
};
-type StoryComponentType = StoryObj;
-
-// Set the default values in the control panel.
-Default.args = {
+const defaultProps = {
...getDefaultFigureForType("point"),
onChangeProps: () => {},
+ onMove: () => {},
onRemove: () => {},
};
+type StoryComponentType = StoryObj;
+
+// Set the default values in the control panel.
+Default.args = defaultProps;
+
export const Controlled: StoryComponentType = {
render: function Render() {
- const [props, setProps] = React.useState({
- ...getDefaultFigureForType("point"),
- onRemove: () => {},
- });
+ const [props, setProps] = React.useState(defaultProps);
const handlePropsUpdate = (newProps) => {
setProps({
@@ -54,10 +54,7 @@ Controlled.parameters = {
export const Expanded: StoryComponentType = {
render: function Render() {
const [expanded, setExpanded] = React.useState(true);
- const [props, setProps] = React.useState({
- ...getDefaultFigureForType("point"),
- onRemove: () => {},
- });
+ const [props, setProps] = React.useState(defaultProps);
const handlePropsUpdate = (newProps) => {
setProps({
@@ -81,12 +78,7 @@ export const Expanded: StoryComponentType = {
export const ExpandedNondefaultProps: StoryComponentType = {
render: function Render() {
const [expanded, setExpanded] = React.useState(true);
- const [props, setProps] = React.useState({
- ...getDefaultFigureForType("point"),
- onRemove: () => {},
- color: "green" as const,
- filled: false,
- });
+ const [props, setProps] = React.useState(defaultProps);
const handlePropsUpdate = (newProps) => {
setProps({
diff --git a/packages/perseus-editor/src/components/__stories__/locked-polygon-settings.stories.tsx b/packages/perseus-editor/src/components/__stories__/locked-polygon-settings.stories.tsx
index 45a6116950..b0836aae80 100644
--- a/packages/perseus-editor/src/components/__stories__/locked-polygon-settings.stories.tsx
+++ b/packages/perseus-editor/src/components/__stories__/locked-polygon-settings.stories.tsx
@@ -14,21 +14,21 @@ export const Default = (args): React.ReactElement => {
return ;
};
-type StoryComponentType = StoryObj;
-
-// Set the default values in the control panel.
-Default.args = {
+const defaultProps = {
...getDefaultFigureForType("polygon"),
onChangeProps: () => {},
+ onMove: () => {},
onRemove: () => {},
};
+type StoryComponentType = StoryObj;
+
+// Set the default values in the control panel.
+Default.args = defaultProps;
+
export const Controlled: StoryComponentType = {
render: function Render() {
- const [props, setProps] = React.useState({
- ...getDefaultFigureForType("polygon"),
- onRemove: () => {},
- });
+ const [props, setProps] = React.useState(defaultProps);
const handlePropsUpdate = (newProps) => {
setProps({
@@ -57,10 +57,7 @@ Controlled.parameters = {
export const Expanded: StoryComponentType = {
render: function Render() {
const [expanded, setExpanded] = React.useState(true);
- const [props, setProps] = React.useState({
- ...getDefaultFigureForType("polygon"),
- onRemove: () => {},
- });
+ const [props, setProps] = React.useState(defaultProps);
const handlePropsUpdate = (newProps) => {
setProps({
diff --git a/packages/perseus-editor/src/components/__stories__/locked-vector-settings.stories.tsx b/packages/perseus-editor/src/components/__stories__/locked-vector-settings.stories.tsx
index d2f765b2a2..4886bf5311 100644
--- a/packages/perseus-editor/src/components/__stories__/locked-vector-settings.stories.tsx
+++ b/packages/perseus-editor/src/components/__stories__/locked-vector-settings.stories.tsx
@@ -14,21 +14,21 @@ export const Default = (args): React.ReactElement => {
return ;
};
-type StoryComponentType = StoryObj;
-
-// Set the default values in the control panel.
-Default.args = {
+const defaultProps = {
...getDefaultFigureForType("vector"),
onChangeProps: () => {},
+ onMove: () => {},
onRemove: () => {},
};
+type StoryComponentType = StoryObj;
+
+// Set the default values in the control panel.
+Default.args = defaultProps;
+
export const Expanded: StoryComponentType = {
render: function Render() {
- const [props, setProps] = React.useState({
- ...getDefaultFigureForType("vector"),
- onRemove: () => {},
- });
+ const [props, setProps] = React.useState(defaultProps);
const handlePropsUpdate = (newProps) => {
setProps({
@@ -54,10 +54,7 @@ export const Expanded: StoryComponentType = {
*/
export const WithInvalidPoints: StoryComponentType = {
render: function Render() {
- const [props, setProps] = React.useState({
- ...getDefaultFigureForType("vector"),
- onRemove: () => {},
- });
+ const [props, setProps] = React.useState(defaultProps);
const handlePropsUpdate = (newProps) => {
setProps({
diff --git a/packages/perseus-editor/src/components/__tests__/locked-ellipse-settings.test.tsx b/packages/perseus-editor/src/components/__tests__/locked-ellipse-settings.test.tsx
index 04e9918758..566307c427 100644
--- a/packages/perseus-editor/src/components/__tests__/locked-ellipse-settings.test.tsx
+++ b/packages/perseus-editor/src/components/__tests__/locked-ellipse-settings.test.tsx
@@ -11,6 +11,7 @@ import type {UserEvent} from "@testing-library/user-event";
const defaultProps = {
...getDefaultFigureForType("ellipse"),
onChangeProps: () => {},
+ onMove: () => {},
onRemove: () => {},
};
diff --git a/packages/perseus-editor/src/components/__tests__/locked-line-settings.test.tsx b/packages/perseus-editor/src/components/__tests__/locked-line-settings.test.tsx
index 26e6f09d0c..32f4986e69 100644
--- a/packages/perseus-editor/src/components/__tests__/locked-line-settings.test.tsx
+++ b/packages/perseus-editor/src/components/__tests__/locked-line-settings.test.tsx
@@ -11,6 +11,7 @@ import type {UserEvent} from "@testing-library/user-event";
const defaultProps = {
...getDefaultFigureForType("line"),
onChangeProps: () => {},
+ onMove: () => {},
onRemove: () => {},
};
diff --git a/packages/perseus-editor/src/components/__tests__/locked-point-settings.test.tsx b/packages/perseus-editor/src/components/__tests__/locked-point-settings.test.tsx
index 7172038840..5ad1cf2a0e 100644
--- a/packages/perseus-editor/src/components/__tests__/locked-point-settings.test.tsx
+++ b/packages/perseus-editor/src/components/__tests__/locked-point-settings.test.tsx
@@ -11,6 +11,7 @@ import type {UserEvent} from "@testing-library/user-event";
const defaultProps = {
...getDefaultFigureForType("point"),
onRemove: () => {},
+ onMove: () => {},
onChangeProps: () => {},
};
diff --git a/packages/perseus-editor/src/components/__tests__/locked-polygon-settings.test.tsx b/packages/perseus-editor/src/components/__tests__/locked-polygon-settings.test.tsx
index a062b483c5..66c3c474e6 100644
--- a/packages/perseus-editor/src/components/__tests__/locked-polygon-settings.test.tsx
+++ b/packages/perseus-editor/src/components/__tests__/locked-polygon-settings.test.tsx
@@ -11,6 +11,7 @@ import type {UserEvent} from "@testing-library/user-event";
const defaultProps = {
...getDefaultFigureForType("polygon"),
onChangeProps: () => {},
+ onMove: () => {},
onRemove: () => {},
};
diff --git a/packages/perseus-editor/src/components/__tests__/locked-vector-settings.test.tsx b/packages/perseus-editor/src/components/__tests__/locked-vector-settings.test.tsx
index a9ff8caf3d..e1005b6968 100644
--- a/packages/perseus-editor/src/components/__tests__/locked-vector-settings.test.tsx
+++ b/packages/perseus-editor/src/components/__tests__/locked-vector-settings.test.tsx
@@ -12,6 +12,7 @@ import type {UserEvent} from "@testing-library/user-event";
const defaultProps = {
...getDefaultFigureForType("vector"),
onChangeProps: () => {},
+ onMove: () => {},
onRemove: () => {},
} as Props;
diff --git a/packages/perseus-editor/src/components/defining-point-settings.tsx b/packages/perseus-editor/src/components/defining-point-settings.tsx
index 17debd6f9a..dba6308f73 100644
--- a/packages/perseus-editor/src/components/defining-point-settings.tsx
+++ b/packages/perseus-editor/src/components/defining-point-settings.tsx
@@ -17,33 +17,41 @@ import CoordinatePairInput from "./coordinate-pair-input";
import LabeledSwitch from "./labeled-switch";
import LockedFigureSettingsAccordion from "./locked-figure-settings-accordion";
-import type {AccordionProps} from "./locked-figure-settings";
import type {LockedPointType} from "@khanacademy/perseus";
-export type Props = AccordionProps &
- LockedPointType & {
- /**
- * Optional label for the point to display in the header summary.
- * Defaults to "Point".
- */
- label: string;
- /**
- * Whether the extra point settings are toggled open.
- */
- showPoint?: boolean;
- /**
- * Optional error message to display.
- */
- error?: string | null;
- /**
- * Called when the extra settings toggle switch is changed.
- */
- onTogglePoint?: (newValue) => void;
- /**
- * Called when the props (coords, color, etc.) are updated.
- */
- onChangeProps: (newProps: Partial) => void;
- };
+export type Props = LockedPointType & {
+ /**
+ * Optional label for the point to display in the header summary.
+ * Defaults to "Point".
+ */
+ label: string;
+ /**
+ * Whether the extra point settings are toggled open.
+ */
+ showPoint?: boolean;
+ /**
+ * Optional error message to display.
+ */
+ error?: string | null;
+ /**
+ * Called when the extra settings toggle switch is changed.
+ */
+ onTogglePoint?: (newValue) => void;
+ /**
+ * Called when the props (coords, color, etc.) are updated.
+ */
+ onChangeProps: (newProps: Partial) => void;
+
+ // Accordion props
+ /**
+ * Whether this accordion is expanded.
+ */
+ expanded?: boolean;
+ /**
+ * Called when the accordion is expanded or collapsed.
+ */
+ onToggle?: (expanded: boolean) => void;
+};
const DefiningPointSettings = (props: Props) => {
const {
@@ -55,6 +63,8 @@ const DefiningPointSettings = (props: Props) => {
error,
onChangeProps,
onTogglePoint,
+ expanded,
+ onToggle,
} = props;
function handleColorChange(newValue) {
@@ -63,8 +73,8 @@ const DefiningPointSettings = (props: Props) => {
return (
void;
/**
* Called when the props (coords, color, etc.) are updated.
*/
@@ -47,6 +43,7 @@ const LockedEllipseSettings = (props: Props) => {
expanded,
onToggle,
onChangeProps,
+ onMove,
onRemove,
} = props;
@@ -162,8 +159,9 @@ const LockedEllipseSettings = (props: Props) => {
{/* Actions */}
);
diff --git a/packages/perseus-editor/src/components/locked-figure-settings-actions.tsx b/packages/perseus-editor/src/components/locked-figure-settings-actions.tsx
index b8def139b6..dcdaa2e3e9 100644
--- a/packages/perseus-editor/src/components/locked-figure-settings-actions.tsx
+++ b/packages/perseus-editor/src/components/locked-figure-settings-actions.tsx
@@ -5,29 +5,76 @@
*/
import Button from "@khanacademy/wonder-blocks-button";
import {View} from "@khanacademy/wonder-blocks-core";
+import IconButton from "@khanacademy/wonder-blocks-icon-button";
+import {Spring} from "@khanacademy/wonder-blocks-layout";
import {spacing} from "@khanacademy/wonder-blocks-tokens";
+import caretDoubleDownIcon from "@phosphor-icons/core/bold/caret-double-down-bold.svg";
+import caretDoubleUpIcon from "@phosphor-icons/core/bold/caret-double-up-bold.svg";
+import caretDownIcon from "@phosphor-icons/core/bold/caret-down-bold.svg";
+import caretUpIcon from "@phosphor-icons/core/bold/caret-up-bold.svg";
import trashIcon from "@phosphor-icons/core/bold/trash-bold.svg";
import {StyleSheet} from "aphrodite";
import * as React from "react";
+import type {LockedFigureType} from "@khanacademy/perseus";
+
+export type LockedFigureSettingsMovementType =
+ | "back"
+ | "backward"
+ | "forward"
+ | "front";
+
type Props = {
+ figureType: LockedFigureType;
+ onMove: (movement: LockedFigureSettingsMovementType) => void;
onRemove: () => void;
- figureAriaLabel: string;
};
const LockedFigureSettingsActions = (props: Props) => {
- const {onRemove, figureAriaLabel} = props;
+ const {figureType, onMove, onRemove} = props;
+
return (
+
+
+
+ onMove("back")}
+ style={styles.iconButton}
+ />
+ onMove("backward")}
+ style={styles.iconButton}
+ />
+ onMove("forward")}
+ style={styles.iconButton}
+ />
+ onMove("front")}
+ style={styles.iconButton}
+ />
);
};
@@ -43,6 +90,11 @@ const styles = StyleSheet.create({
// Line up the delete icon with the rest of the content.
marginInlineStart: -spacing.xxxSmall_4,
},
+ iconButton: {
+ // Reset the margin because the icon buttons
+ // overlap each other otherwise.
+ margin: 0,
+ },
});
export default LockedFigureSettingsActions;
diff --git a/packages/perseus-editor/src/components/locked-figure-settings.tsx b/packages/perseus-editor/src/components/locked-figure-settings.tsx
index ed3987e07b..9ca354b459 100644
--- a/packages/perseus-editor/src/components/locked-figure-settings.tsx
+++ b/packages/perseus-editor/src/components/locked-figure-settings.tsx
@@ -14,15 +14,28 @@ import LockedPolygonSettings from "./locked-polygon-settings";
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 LockedLineProps} from "./locked-line-settings";
import type {Props as LockedPointProps} from "./locked-point-settings";
import type {Props as LockedPolygonProps} from "./locked-polygon-settings";
import type {Props as LockedVectorProps} from "./locked-vector-settings";
-export type AccordionProps = {
+export type LockedFigureSettingsCommonProps = {
// Whether to show the M2 features in the locked figure settings.
// TODO(LEMS-2016): Remove this prop once the M2 flag is fully rolled out.
showM2Features?: boolean;
+
+ // Movement props
+ /**
+ * Called when a movement button (top, up, down, bottom) is pressed.
+ */
+ onMove: (movement: LockedFigureSettingsMovementType) => void;
+ /**
+ * Called when the delete button is pressed.
+ */
+ onRemove: () => void;
+
+ // Accordion props
/**
* Whether this accordion is expanded.
*/
@@ -34,7 +47,7 @@ export type AccordionProps = {
};
// Union this type with other locked figure types when they are added.
-type Props = AccordionProps &
+type Props = LockedFigureSettingsCommonProps &
(
| LockedPointProps
| LockedLineProps
diff --git a/packages/perseus-editor/src/components/locked-figures-section.tsx b/packages/perseus-editor/src/components/locked-figures-section.tsx
index 748d266a28..7770cda492 100644
--- a/packages/perseus-editor/src/components/locked-figures-section.tsx
+++ b/packages/perseus-editor/src/components/locked-figures-section.tsx
@@ -14,6 +14,7 @@ import LockedFigureSelect from "./locked-figure-select";
import LockedFigureSettings from "./locked-figure-settings";
import {getDefaultFigureForType} from "./util";
+import type {LockedFigureSettingsMovementType} from "./locked-figure-settings-actions";
import type {Props as InteractiveGraphEditorProps} from "../widgets/interactive-graph-editor";
import type {LockedFigure, LockedFigureType} from "@khanacademy/perseus";
@@ -47,6 +48,56 @@ const LockedFiguresSection = (props: Props) => {
setExpandedStates([...expandedStates, true]);
}
+ function moveLockedFigure(
+ index: number,
+ movement: LockedFigureSettingsMovementType,
+ ) {
+ // Don't allow moving the first figure up or the last figure down.
+ if (index === 0 && (movement === "back" || movement === "backward")) {
+ return;
+ }
+ if (
+ figures &&
+ index === figures.length - 1 &&
+ (movement === "front" || movement === "forward")
+ ) {
+ return;
+ }
+
+ const lockedFigures = figures || [];
+ const newFigures = [...lockedFigures];
+ const newExpandedStates = [...expandedStates];
+
+ // First, remove the figure from its current position
+ // in the figures array and the expanded states array.
+ const [removedFigure] = newFigures.splice(index, 1);
+ newExpandedStates.splice(index, 1);
+
+ // Then, add it back in the new position. Add "true" to the
+ // expanded states array for the new position (it must already
+ // be open since the button to move it is being pressed from there).
+ switch (movement) {
+ case "back":
+ newFigures.unshift(removedFigure);
+ newExpandedStates.unshift(true);
+ break;
+ case "backward":
+ newFigures.splice(index - 1, 0, removedFigure);
+ newExpandedStates.splice(index - 1, 0, true);
+ break;
+ case "forward":
+ newFigures.splice(index + 1, 0, removedFigure);
+ newExpandedStates.splice(index + 1, 0, true);
+ break;
+ case "front":
+ newFigures.push(removedFigure);
+ newExpandedStates.push(true);
+ break;
+ }
+ onChange({lockedFigures: newFigures});
+ setExpandedStates(newExpandedStates);
+ }
+
function removeLockedFigure(index: number) {
// eslint-disable-next-line no-alert
if (window.confirm("Are you sure you want to delete this figure?")) {
@@ -110,6 +161,7 @@ const LockedFiguresSection = (props: Props) => {
onChangeProps={(newProps) =>
changeProps(index, newProps)
}
+ onMove={(movement) => moveLockedFigure(index, movement)}
onRemove={() => removeLockedFigure(index)}
/>
);
diff --git a/packages/perseus-editor/src/components/locked-line-settings.tsx b/packages/perseus-editor/src/components/locked-line-settings.tsx
index 94ed532adc..08aeb77c71 100644
--- a/packages/perseus-editor/src/components/locked-line-settings.tsx
+++ b/packages/perseus-editor/src/components/locked-line-settings.tsx
@@ -19,7 +19,7 @@ import LineSwatch from "./line-swatch";
import LockedFigureSettingsAccordion from "./locked-figure-settings-accordion";
import LockedFigureSettingsActions from "./locked-figure-settings-actions";
-import type {AccordionProps} from "./locked-figure-settings";
+import type {LockedFigureSettingsCommonProps} from "./locked-figure-settings";
import type {
LockedFigure,
LockedFigureColor,
@@ -30,11 +30,7 @@ import type {
const lengthZeroStr = "The line cannot have length 0.";
export type Props = LockedLineType &
- AccordionProps & {
- /**
- * Called when the delete button is pressed.
- */
- onRemove: () => void;
+ LockedFigureSettingsCommonProps & {
/**
* Called when the props (points, color, etc.) are updated.
*/
@@ -50,6 +46,7 @@ const LockedLineSettings = (props: Props) => {
showPoint1,
showPoint2,
onChangeProps,
+ onMove,
onRemove,
} = props;
const [point1, point2] = points;
@@ -180,10 +177,9 @@ const LockedLineSettings = (props: Props) => {
{/* Actions */}
);
diff --git a/packages/perseus-editor/src/components/locked-point-settings.tsx b/packages/perseus-editor/src/components/locked-point-settings.tsx
index 0ff6de3dd7..5cfa2f868d 100644
--- a/packages/perseus-editor/src/components/locked-point-settings.tsx
+++ b/packages/perseus-editor/src/components/locked-point-settings.tsx
@@ -18,15 +18,11 @@ import LabeledSwitch from "./labeled-switch";
import LockedFigureSettingsAccordion from "./locked-figure-settings-accordion";
import LockedFigureSettingsActions from "./locked-figure-settings-actions";
-import type {AccordionProps} from "./locked-figure-settings";
+import type {LockedFigureSettingsCommonProps} from "./locked-figure-settings";
import type {LockedPointType} from "@khanacademy/perseus";
-export type Props = AccordionProps &
+export type Props = LockedFigureSettingsCommonProps &
LockedPointType & {
- /**
- * Called when the delete button is pressed.
- */
- onRemove: () => void;
/**
* Called when the props (coords, color, etc.) are updated.
*/
@@ -39,6 +35,7 @@ const LockedPointSettings = (props: Props) => {
color: pointColor,
filled = true,
onChangeProps,
+ onMove,
onRemove,
} = props;
@@ -82,8 +79,9 @@ const LockedPointSettings = (props: Props) => {
/>
);
diff --git a/packages/perseus-editor/src/components/locked-polygon-settings.tsx b/packages/perseus-editor/src/components/locked-polygon-settings.tsx
index 8bfbdd5dc0..9363448067 100644
--- a/packages/perseus-editor/src/components/locked-polygon-settings.tsx
+++ b/packages/perseus-editor/src/components/locked-polygon-settings.tsx
@@ -24,14 +24,10 @@ import LockedFigureSettingsAccordion from "./locked-figure-settings-accordion";
import LockedFigureSettingsActions from "./locked-figure-settings-actions";
import PolygonSwatch from "./polygon-swatch";
-import type {AccordionProps} from "./locked-figure-settings";
+import type {LockedFigureSettingsCommonProps} from "./locked-figure-settings";
-export type Props = AccordionProps &
+export type Props = LockedFigureSettingsCommonProps &
LockedPolygonType & {
- /**
- * Called when the delete button is pressed.
- */
- onRemove: () => void;
/**
* Called when the props (coords, color, etc.) are updated.
*/
@@ -48,6 +44,8 @@ const LockedPolygonSettings = (props: Props) => {
expanded,
onToggle,
onChangeProps,
+ onMove,
+ onRemove,
} = props;
function handleColorChange(newValue: LockedFigureColor) {
@@ -199,8 +197,9 @@ const LockedPolygonSettings = (props: Props) => {
{/* Actions */}
);
diff --git a/packages/perseus-editor/src/components/locked-vector-settings.tsx b/packages/perseus-editor/src/components/locked-vector-settings.tsx
index d3a2ea9ac2..ba4253622a 100644
--- a/packages/perseus-editor/src/components/locked-vector-settings.tsx
+++ b/packages/perseus-editor/src/components/locked-vector-settings.tsx
@@ -18,7 +18,7 @@ import LineSwatch from "./line-swatch";
import LockedFigureSettingsAccordion from "./locked-figure-settings-accordion";
import LockedFigureSettingsActions from "./locked-figure-settings-actions";
-import type {AccordionProps} from "./locked-figure-settings";
+import type {LockedFigureSettingsCommonProps} from "./locked-figure-settings";
import type {
Coord,
LockedFigure,
@@ -29,11 +29,7 @@ import type {
const lengthErrorMessage = "The vector cannot have length 0.";
export type Props = LockedVectorType &
- AccordionProps & {
- /**
- * Called when the delete button is pressed.
- */
- onRemove: () => void;
+ LockedFigureSettingsCommonProps & {
/**
* Called when the props (points, color, etc.) are updated.
*/
@@ -41,7 +37,7 @@ export type Props = LockedVectorType &
};
const LockedVectorSettings = (props: Props) => {
- const {points, color: lineColor, onChangeProps, onRemove} = props;
+ const {points, color: lineColor, onChangeProps, onMove, onRemove} = props;
const [tail, tip] = points;
const lineLabel = `Vector (${tail[0]}, ${tail[1]}), (${tip[0]}, ${tip[1]})`;
@@ -132,10 +128,9 @@ const LockedVectorSettings = (props: Props) => {
{/* Actions */}
);
diff --git a/packages/perseus-editor/src/widgets/__tests__/interactive-graph-editor-locked-figures.test.tsx b/packages/perseus-editor/src/widgets/__tests__/interactive-graph-editor-locked-figures.test.tsx
index 26cc84aa8b..e5cd7caa54 100644
--- a/packages/perseus-editor/src/widgets/__tests__/interactive-graph-editor-locked-figures.test.tsx
+++ b/packages/perseus-editor/src/widgets/__tests__/interactive-graph-editor-locked-figures.test.tsx
@@ -97,7 +97,7 @@ describe("InteractiveGraphEditor locked figures", () => {
// Act
const deleteButton = screen.getByRole("button", {
- name: /Delete/,
+ name: `Delete locked ${figureType}`,
});
await userEvent.click(deleteButton);
@@ -125,7 +125,7 @@ describe("InteractiveGraphEditor locked figures", () => {
// Act
const deleteButton = screen.getByRole("button", {
- name: /Delete/,
+ name: `Delete locked ${figureType}`,
});
await userEvent.click(deleteButton);
@@ -134,6 +134,189 @@ describe("InteractiveGraphEditor locked figures", () => {
expect(onChangeMock).not.toBeCalled();
});
+ describe("movement", () => {
+ const first = {
+ ...getDefaultFigureForType(figureType),
+ color: "blue",
+ };
+ const second = {
+ ...getDefaultFigureForType(figureType),
+ color: "green",
+ };
+ const third = {
+ ...getDefaultFigureForType(figureType),
+ color: "red",
+ };
+
+ test(`Calls onChange when a locked ${figureType} is moved back`, async () => {
+ // Arrange
+ const onChangeMock = jest.fn();
+
+ renderEditor({
+ onChange: onChangeMock,
+ lockedFigures: [first, second, third],
+ });
+
+ // Act
+ const moveButton = screen.getAllByRole("button", {
+ name: `Move locked ${figureType} to the back`,
+ });
+ await userEvent.click(moveButton[2]);
+
+ // Assert
+ expect(onChangeMock).toBeCalledWith(
+ expect.objectContaining({
+ lockedFigures: [third, first, second],
+ }),
+ );
+ });
+
+ test(`Calls onChange when a locked ${figureType} is moved backward`, async () => {
+ // Arrange
+ const onChangeMock = jest.fn();
+
+ renderEditor({
+ onChange: onChangeMock,
+ lockedFigures: [first, second, third],
+ });
+
+ // Act
+ const moveButton = screen.getAllByRole("button", {
+ name: `Move locked ${figureType} backward`,
+ });
+ await userEvent.click(moveButton[2]);
+
+ // Assert
+ expect(onChangeMock).toBeCalledWith(
+ expect.objectContaining({
+ lockedFigures: [first, third, second],
+ }),
+ );
+ });
+
+ test(`Calls onChange when a locked ${figureType} is moved forward`, async () => {
+ // Arrange
+ const onChangeMock = jest.fn();
+
+ renderEditor({
+ onChange: onChangeMock,
+ lockedFigures: [first, second, third],
+ });
+
+ // Act
+ const moveButton = screen.getAllByRole("button", {
+ name: `Move locked ${figureType} forward`,
+ });
+ await userEvent.click(moveButton[0]);
+
+ // Assert
+ expect(onChangeMock).toBeCalledWith(
+ expect.objectContaining({
+ lockedFigures: [second, first, third],
+ }),
+ );
+ });
+
+ test(`Calls onChange when a locked ${figureType} is moved front`, async () => {
+ // Arrange
+ const onChangeMock = jest.fn();
+
+ renderEditor({
+ onChange: onChangeMock,
+ lockedFigures: [first, second, third],
+ });
+
+ // Act
+ const moveButton = screen.getAllByRole("button", {
+ name: `Move locked ${figureType} to the front`,
+ });
+ await userEvent.click(moveButton[0]);
+
+ // Assert
+ expect(onChangeMock).toBeCalledWith(
+ expect.objectContaining({
+ lockedFigures: [second, third, first],
+ }),
+ );
+ });
+
+ test(`Does not call onChange when a locked ${figureType} is moved to the back and is already in the back`, async () => {
+ // Arrange
+ const onChangeMock = jest.fn();
+
+ renderEditor({
+ onChange: onChangeMock,
+ lockedFigures: [first, second, third],
+ });
+
+ // Act
+ const moveButton = screen.getAllByRole("button", {
+ name: `Move locked ${figureType} to the back`,
+ });
+ await userEvent.click(moveButton[0]);
+
+ // Assert
+ expect(onChangeMock).not.toBeCalled();
+ });
+
+ test(`Does not call onChange when a locked ${figureType} is moved backward and is already in the back`, async () => {
+ // Arrange
+ const onChangeMock = jest.fn();
+
+ renderEditor({
+ onChange: onChangeMock,
+ lockedFigures: [first, second, third],
+ });
+
+ // Act
+ const moveButton = screen.getAllByRole("button", {
+ name: `Move locked ${figureType} backward`,
+ });
+ await userEvent.click(moveButton[0]);
+
+ // Assert
+ expect(onChangeMock).not.toBeCalled();
+ });
+
+ test(`Does not call onChange when a locked ${figureType} is moved forward and is already in the front`, async () => {
+ // Arrange
+ const onChangeMock = jest.fn();
+
+ renderEditor({
+ onChange: onChangeMock,
+ lockedFigures: [first, second, third],
+ });
+
+ // Act
+ const moveButton = screen.getAllByRole("button", {
+ name: `Move locked ${figureType} forward`,
+ });
+ await userEvent.click(moveButton[2]);
+
+ // Assert
+ expect(onChangeMock).not.toBeCalled();
+ });
+
+ test(`Does not call onChange when a locked ${figureType} is moved to the front and is already in the front`, async () => {
+ // Arrange
+ const onChangeMock = jest.fn();
+
+ renderEditor({
+ onChange: onChangeMock,
+ lockedFigures: [first, second, third],
+ });
+
+ // Act
+ const moveButton = screen.getAllByRole("button", {
+ name: `Move locked ${figureType} to the front`,
+ });
+ await userEvent.click(moveButton[2]);
+
+ // Assert
+ expect(onChangeMock).not.toBeCalled();
+ });
+ });
+
test("Shows settings accordion when a locked $figureType is passed in", async () => {
// Arrange
renderEditor({
@@ -996,7 +1179,7 @@ describe("InteractiveGraphEditor locked figures", () => {
// Delete the figure
const deleteButton = screen.getByRole("button", {
- name: "Delete locked line defined by 0, 0 and 2, 2.",
+ name: "Delete locked line",
});
await userEvent.click(deleteButton);