diff --git a/.changeset/itchy-pears-march.md b/.changeset/itchy-pears-march.md
new file mode 100644
index 0000000000..3b7781655a
--- /dev/null
+++ b/.changeset/itchy-pears-march.md
@@ -0,0 +1,6 @@
+---
+"@khanacademy/perseus": minor
+"@khanacademy/perseus-editor": minor
+---
+
+[Locked Figure Aria] Locked line aria label editor UI
diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-line-settings.test.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-line-settings.test.tsx
index 92ed774f76..d5b5e345cc 100644
--- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-line-settings.test.tsx
+++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-line-settings.test.tsx
@@ -550,4 +550,161 @@ describe("LockedLineSettings", () => {
});
});
});
+
+ describe("Aria label", () => {
+ test("Renders with aria label", () => {
+ // Arrange
+
+ // Act
+ render(
+ ,
+ {wrapper: RenderStateRoot},
+ );
+
+ const input = screen.getByRole("textbox", {name: "Aria label"});
+
+ // Assert
+ expect(input).toHaveValue("Line at (x, y)");
+ });
+
+ test("calls onChangeProps when the aria label is updated", async () => {
+ // Arrange
+ const onChangeProps = jest.fn();
+ render(
+ ,
+ {wrapper: RenderStateRoot},
+ );
+
+ // Act
+ const input = screen.getByRole("textbox", {name: "Aria label"});
+ await userEvent.clear(input);
+ await userEvent.type(input, "A");
+
+ // Assert
+ expect(onChangeProps).toHaveBeenCalledWith({
+ ariaLabel: "A",
+ });
+ });
+
+ test("aria label auto-generates with different kind", async () => {
+ // Arrange
+ const onChangeProps = jest.fn();
+ render(
+ ,
+ {wrapper: RenderStateRoot},
+ );
+
+ // Act
+ const autoGenButton = screen.getByRole("button", {
+ name: "Auto-generate",
+ });
+ await userEvent.click(autoGenButton);
+
+ // Assert
+ expect(onChangeProps).toHaveBeenCalledWith({
+ ariaLabel: "Segment from (0, 0) to (2, 2)",
+ });
+ });
+
+ test("aria label auto-generates (no labels)", async () => {
+ // Arrange
+ const onChangeProps = jest.fn();
+
+ // Act
+ render(
+ ,
+ {wrapper: RenderStateRoot},
+ );
+
+ const autoGenButton = screen.getByRole("button", {
+ name: "Auto-generate",
+ });
+ await userEvent.click(autoGenButton);
+
+ // Assert
+ expect(onChangeProps).toHaveBeenCalledWith({
+ ariaLabel: "Line from (0, 0) to (2, 2)",
+ });
+ });
+
+ test("aria label auto-generates (one label)", async () => {
+ // Arrange
+ const onChangeProps = jest.fn();
+ render(
+ ,
+ {wrapper: RenderStateRoot},
+ );
+
+ // Act
+ const autoGenButton = screen.getByRole("button", {
+ name: "Auto-generate",
+ });
+ await userEvent.click(autoGenButton);
+
+ // Assert
+ expect(onChangeProps).toHaveBeenCalledWith({
+ ariaLabel: "Line from (0, 0) to (2, 2) with label A",
+ });
+ });
+
+ test("aria label auto-generates (multiple labels)", async () => {
+ // Arrange
+ const onChangeProps = jest.fn();
+ render(
+ ,
+ {wrapper: RenderStateRoot},
+ );
+
+ // Act
+ const autoGenButton = screen.getByRole("button", {
+ name: "Auto-generate",
+ });
+ await userEvent.click(autoGenButton);
+
+ // Assert
+ expect(onChangeProps).toHaveBeenCalledWith({
+ ariaLabel: "Line from (0, 0) to (2, 2) with labels A, B",
+ });
+ });
+ });
});
diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-line-settings.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-line-settings.tsx
index 563e82be4f..5bf69ad6ac 100644
--- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-line-settings.tsx
+++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-line-settings.tsx
@@ -21,6 +21,7 @@ import PerseusEditorAccordion from "../../../components/perseus-editor-accordion
import ColorSelect from "./color-select";
import LineStrokeSelect from "./line-stroke-select";
import LineSwatch from "./line-swatch";
+import LockedFigureAria from "./locked-figure-aria";
import LockedFigureSettingsActions from "./locked-figure-settings-actions";
import LockedLabelSettings from "./locked-label-settings";
import LockedPointSettings from "./locked-point-settings";
@@ -56,6 +57,7 @@ const LockedLineSettings = (props: Props) => {
showPoint1,
showPoint2,
labels,
+ ariaLabel,
onChangeProps,
onMove,
onRemove,
@@ -69,6 +71,24 @@ const LockedLineSettings = (props: Props) => {
// Check if the line has length 0.
const isInvalid = kvector.equal(point1.coord, point2.coord);
+ function getPrepopulatedAriaLabel() {
+ let str = `${capitalizeKind} from (${point1.coord[0]}, ${point1.coord[1]}) to (${point2.coord[0]}, ${point2.coord[1]})`;
+
+ if (labels && labels.length > 0) {
+ str += " with label";
+ // Make it "with labels" instead of "with label" if there are
+ // multiple labels.
+ if (labels.length > 1) {
+ str += "s";
+ }
+
+ // Separate additional labels with commas.
+ str += ` ${labels.map((l) => l.text).join(", ")}`;
+ }
+
+ return str;
+ }
+
function handleChangePoint(
newPointProps: Partial,
index: 0 | 1,
@@ -243,9 +263,28 @@ const LockedLineSettings = (props: Props) => {
onChangeProps={(newProps) => handleChangePoint(newProps, 1)}
/>
+ {flags?.["mafs"]?.["locked-figures-aria"] && (
+ <>
+
+
+
+ {
+ onChangeProps(newProps);
+ }}
+ />
+ >
+ )}
+
{flags?.["mafs"]?.["locked-line-labels"] && (
<>
+
+
+
+ Visible labels
{labels?.map((label, labelIndex) => (