-
Notifications
You must be signed in to change notification settings - Fork 350
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create UI for adding a locked point to interactive graph editor (mafs…
… only) (#1074) ## Summary: UI for adding a locked point to interactive graph editor! This will only be for mafs graphs as mafs graphs have a slightly different look and API (and also it was just not working very well with graphie anyway). This UI includes: - A dropdown that allows adding elements (just "Point" for now) added to the interactive graph editor. - Once added, settings for points - The ability to delete the setting in question - The x and y coordinates of the point - The removal of the locked point investigation from graph.tsx and related files. [The ability to change the color of a point](https://khanacademy.atlassian.net/browse/LEMS-1751) is coming in a future PR. Issue: LEMS-1813 ## Test plan: `yarn jest packages/perseus-editor/src/widgets/__tests__/interactive-graph-editor.test.tsx` `yarn jest packages/perseus-editor/src/components/__tests__/util.test.ts` Storybook - Go to http://localhost:6006/?path=/story/perseus-editor-widgets-interactive-graph-editor--with-locked-points - Scroll down to the "Add an element" dropdown - Click "Add an element" - Click "Point" - Confirm that a black point appears at (0, 0) on the graph - Change the coordinates and confirm that the point moves - Add another point - Use the trash button to delete a point - Confirm that the correct point is deleted ## Demo: https://github.com/Khan/perseus/assets/13231763/9eca725b-651a-4312-b639-bda26163ab8e Author: nishasy Reviewers: nishasy, nedredmond, jeremywiebe, benchristel, mark-fitzgerald Required Reviewers: Approved By: nedredmond Checks: ✅ codecov/project, ✅ codecov/patch, ✅ gerald, ✅ Upload Coverage, ⏭️ Publish npm snapshot, ✅ Extract i18n strings (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Jest Coverage (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: #1074
- Loading branch information
Showing
18 changed files
with
647 additions
and
73 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
"@khanacademy/perseus": minor | ||
"@khanacademy/perseus-editor": minor | ||
--- | ||
|
||
Add "add a locked figure" UI to interactive graph editor + adding points (mafs graphs only) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
42 changes: 42 additions & 0 deletions
42
packages/perseus-editor/src/components/__tests__/util.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import {getValidNumberFromString, getDefaultFigureForFigureType} from "../util"; | ||
|
||
describe("getValidNumberFromString", () => { | ||
test("should return a number from a string", () => { | ||
expect(getValidNumberFromString("123")).toBe(123); | ||
}); | ||
|
||
test("should return a negative number from a string", () => { | ||
expect(getValidNumberFromString("-123")).toBe(-123); | ||
}); | ||
|
||
test("should return 0 from an invalid string", () => { | ||
expect(getValidNumberFromString("abc")).toBe(0); | ||
}); | ||
|
||
test("should return 0 from an empty string", () => { | ||
expect(getValidNumberFromString("")).toBe(0); | ||
}); | ||
|
||
test("should return 0 from a string with spaces", () => { | ||
expect(getValidNumberFromString(" ")).toBe(0); | ||
}); | ||
|
||
test("should return the number from a combo of numbers followed by letters", () => { | ||
expect(getValidNumberFromString("123abc")).toBe(123); | ||
}); | ||
|
||
test("should return 0 from a combo of letters followed by numbers", () => { | ||
expect(getValidNumberFromString("abc123")).toBe(0); | ||
}); | ||
|
||
test("should return the first number from a mixed combo of numbers and letters", () => { | ||
expect(getValidNumberFromString("123abc123")).toBe(123); | ||
}); | ||
}); | ||
|
||
describe("getDefaultFigureForFigureType", () => { | ||
test("should return a point with default coordinates", () => { | ||
const figure = getDefaultFigureForFigureType("point"); | ||
expect(figure).toEqual({type: "point", coord: [0, 0]}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
49 changes: 49 additions & 0 deletions
49
packages/perseus-editor/src/components/locked-figure-select.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
/** | ||
* Dropdown for selecting a locked figure to add to an interactive graph. | ||
* Locked figures are elements (points, segmeents, etc.) that are not | ||
* interactive, just present in the graph's background. | ||
* | ||
* Used in the interactive graph editor's locked figures section. | ||
*/ | ||
import {View} from "@khanacademy/wonder-blocks-core"; | ||
import {ActionItem, ActionMenu} from "@khanacademy/wonder-blocks-dropdown"; | ||
import {spacing, color} from "@khanacademy/wonder-blocks-tokens"; | ||
import {StyleSheet} from "aphrodite"; | ||
import * as React from "react"; | ||
|
||
type Props = { | ||
id: string; | ||
onChange: (value: string) => void; | ||
}; | ||
|
||
const LockedFigureSelect = (props: Props) => { | ||
const {id, onChange} = props; | ||
|
||
return ( | ||
<View style={styles.container}> | ||
<ActionMenu menuText="Add element" style={styles.addElementSelect}> | ||
{[ | ||
<ActionItem | ||
key={`${id}-point`} | ||
label="Point" | ||
onClick={() => onChange("point")} | ||
> | ||
Point | ||
</ActionItem>, | ||
]} | ||
</ActionMenu> | ||
</View> | ||
); | ||
}; | ||
|
||
const styles = StyleSheet.create({ | ||
container: { | ||
marginTop: spacing.medium_16, | ||
}, | ||
addElementSelect: { | ||
backgroundColor: color.fadedBlue16, | ||
borderRadius: spacing.xxxSmall_4, | ||
}, | ||
}); | ||
|
||
export default LockedFigureSelect; |
26 changes: 26 additions & 0 deletions
26
packages/perseus-editor/src/components/locked-figure-settings.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
/** | ||
* LockedFigureSettings takes in a figure and renders the | ||
* respective settings for that figure type. | ||
* | ||
* Used in the interactive graph editor's locked figures section. | ||
*/ | ||
|
||
import * as React from "react"; | ||
|
||
import LockedPointSettings from "./locked-point-settings"; | ||
|
||
import type {Props as LockedPointProps} from "./locked-point-settings"; | ||
|
||
// Union this type with other locked figure types when they are added. | ||
type Props = LockedPointProps; | ||
|
||
const LockedFigureSettings = (props: Props) => { | ||
switch (props.type) { | ||
case "point": | ||
return <LockedPointSettings {...props} />; | ||
} | ||
|
||
return null; | ||
}; | ||
|
||
export default LockedFigureSettings; |
87 changes: 87 additions & 0 deletions
87
packages/perseus-editor/src/components/locked-figures-section.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
/** | ||
* LockedFiguresSection is a section of the InteractiveGraphEditor that allows | ||
* the user to add and remove locked figures from the graph. It includes | ||
* the dropdown for adding figures as well as the settings for each figure. | ||
*/ | ||
import {View, useUniqueIdWithMock} from "@khanacademy/wonder-blocks-core"; | ||
import {spacing} from "@khanacademy/wonder-blocks-tokens"; | ||
import {StyleSheet} from "aphrodite"; | ||
import * as React from "react"; | ||
|
||
import LockedFigureSelect from "./locked-figure-select"; | ||
import LockedFigureSettings from "./locked-figure-settings"; | ||
import {getDefaultFigureForFigureType} from "./util"; | ||
|
||
import type {Props as InteractiveGraphEditorProps} from "../widgets/interactive-graph-editor"; | ||
import type {LockedFigure, LockedFigureType} from "@khanacademy/perseus"; | ||
|
||
type Props = { | ||
figures?: Array<LockedFigure>; | ||
onChange: (props: Partial<InteractiveGraphEditorProps>) => void; | ||
}; | ||
|
||
const LockedFiguresSection = (props: Props) => { | ||
const uniqueId = useUniqueIdWithMock().get("locked-figures-section"); | ||
const {figures, onChange} = props; | ||
|
||
function addLockedFigure(newFigure: LockedFigureType) { | ||
const lockedFigures = figures || []; | ||
const newProps = { | ||
lockedFigures: [ | ||
...lockedFigures, | ||
getDefaultFigureForFigureType(newFigure), | ||
], | ||
}; | ||
onChange(newProps); | ||
} | ||
|
||
function removeLockedFigure(index: number) { | ||
const lockedFigures = figures || []; | ||
onChange({ | ||
lockedFigures: [ | ||
...lockedFigures.slice(0, index), | ||
...lockedFigures.slice(index + 1), | ||
], | ||
}); | ||
} | ||
|
||
function changeCoord(index: number, coord: [number, number]) { | ||
const lockedFigures = figures || []; | ||
const newProps = { | ||
lockedFigures: [ | ||
...lockedFigures.slice(0, index), | ||
{ | ||
...lockedFigures[index], | ||
coord, | ||
}, | ||
...lockedFigures.slice(index + 1), | ||
], | ||
}; | ||
onChange(newProps); | ||
} | ||
|
||
return ( | ||
<View style={styles.container}> | ||
{figures?.map((figure, index) => ( | ||
<LockedFigureSettings | ||
key={`${uniqueId}-locked-${figure}-${index}`} | ||
{...figure} | ||
onRemove={() => removeLockedFigure(index)} | ||
onChangeCoord={(coord) => changeCoord(index, coord)} | ||
/> | ||
))} | ||
<LockedFigureSelect | ||
id={`${uniqueId}-select`} | ||
onChange={addLockedFigure} | ||
/> | ||
</View> | ||
); | ||
}; | ||
|
||
const styles = StyleSheet.create({ | ||
container: { | ||
paddingTop: spacing.large_24, | ||
}, | ||
}); | ||
|
||
export default LockedFiguresSection; |
131 changes: 131 additions & 0 deletions
131
packages/perseus-editor/src/components/locked-point-settings.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
/** | ||
* LockedPointSettings is a component that allows the user to edit the | ||
* settings of specifically a locked point on the graph. | ||
* | ||
* Used in the interactive graph editor's locked figures section. | ||
*/ | ||
import {View, useUniqueIdWithMock} from "@khanacademy/wonder-blocks-core"; | ||
import {TextField} from "@khanacademy/wonder-blocks-form"; | ||
import IconButton from "@khanacademy/wonder-blocks-icon-button"; | ||
import {Spring} from "@khanacademy/wonder-blocks-layout"; | ||
import {color, spacing} from "@khanacademy/wonder-blocks-tokens"; | ||
import {LabelMedium, LabelLarge} from "@khanacademy/wonder-blocks-typography"; | ||
import trashIcon from "@phosphor-icons/core/bold/trash-bold.svg"; | ||
import {StyleSheet} from "aphrodite"; | ||
import * as React from "react"; | ||
|
||
import {getValidNumberFromString} from "./util"; | ||
|
||
import type {LockedPoint} from "@khanacademy/perseus"; | ||
|
||
export type Props = LockedPoint & { | ||
onRemove: () => void; | ||
onChangeCoord: (coord: [number, number]) => void; | ||
}; | ||
|
||
const LockedPointSettings = (props: Props) => { | ||
const {coord, onRemove, onChangeCoord} = props; | ||
const [coordState, setCoordState] = React.useState([ | ||
// Using strings to make it easier to work with the text fields. | ||
coord[0].toString(), | ||
coord[1].toString(), | ||
]); | ||
|
||
// Generate unique IDs so that the programmatic labels can be associated | ||
// with their respective text fields. | ||
const ids = useUniqueIdWithMock(); | ||
const xCoordId = ids.get("x-coord"); | ||
const yCoordId = ids.get("y-coord"); | ||
|
||
function handleBlur() { | ||
const validCoord = [ | ||
getValidNumberFromString(coordState[0]), | ||
getValidNumberFromString(coordState[1]), | ||
] as [number, number]; | ||
|
||
// Make the text field only show valid numbers after blur. | ||
setCoordState([validCoord[0].toString(), validCoord[1].toString()]); | ||
// Update the graph with the new coordinates. | ||
onChangeCoord(validCoord); | ||
} | ||
|
||
function handleChange(newValue, coordIndex) { | ||
const newCoord = [...coordState]; | ||
newCoord[coordIndex] = newValue; | ||
setCoordState(newCoord); | ||
} | ||
|
||
return ( | ||
<View style={styles.container}> | ||
{/* Title row */} | ||
<View style={styles.row}> | ||
<LabelLarge>Point</LabelLarge> | ||
<Spring /> | ||
<IconButton | ||
icon={trashIcon} | ||
aria-label={`Delete locked point at ${coordState[0]}, ${coordState[1]}`} | ||
onClick={onRemove} | ||
/> | ||
</View> | ||
|
||
{/* Coordinates */} | ||
<View> | ||
<View style={styles.row}> | ||
<LabelMedium | ||
htmlFor={xCoordId} | ||
style={styles.label} | ||
tag="label" | ||
> | ||
x Coordinate | ||
</LabelMedium> | ||
<TextField | ||
id={xCoordId} | ||
value={coordState[0]} | ||
onChange={(newValue) => handleChange(newValue, 0)} | ||
onBlur={handleBlur} | ||
style={styles.textField} | ||
/> | ||
</View> | ||
|
||
<View style={styles.row}> | ||
<LabelMedium | ||
htmlFor={yCoordId} | ||
style={styles.label} | ||
tag="label" | ||
> | ||
y Coordinate | ||
</LabelMedium> | ||
<TextField | ||
id={yCoordId} | ||
value={coordState[1]} | ||
onChange={(newValue) => handleChange(newValue, 1)} | ||
onBlur={handleBlur} | ||
style={styles.textField} | ||
/> | ||
</View> | ||
</View> | ||
</View> | ||
); | ||
}; | ||
|
||
const styles = StyleSheet.create({ | ||
container: { | ||
backgroundColor: color.fadedBlue8, | ||
marginBottom: spacing.xSmall_8, | ||
padding: spacing.medium_16, | ||
borderRadius: spacing.xSmall_8, | ||
}, | ||
row: { | ||
flexDirection: "row", | ||
alignItems: "center", | ||
marginBottom: spacing.xSmall_8, | ||
}, | ||
label: { | ||
marginInlineEnd: spacing.xSmall_8, | ||
}, | ||
textField: { | ||
width: spacing.xxxLarge_64, | ||
}, | ||
}); | ||
|
||
export default LockedPointSettings; |
Oops, something went wrong.