From 10032567a76c3d4c20c79e5333b6200e909dacd9 Mon Sep 17 00:00:00 2001 From: Nisha Yerunkar Date: Thu, 26 Sep 2024 14:56:48 -0700 Subject: [PATCH] [Locked Figure Aria] Implement locked vector aria labels (graph + editor) --- .changeset/red-garlics-divide.md | 6 + .../locked-vector-settings.test.tsx | 132 ++++++++++++++++++ .../locked-figures/locked-vector-settings.tsx | 46 +++++- packages/perseus/src/perseus-types.ts | 1 + .../interactive-graphs/graph-locked-layer.tsx | 6 +- ...interactive-graph-question-builder.test.ts | 2 + .../interactive-graph-question-builder.ts | 2 + .../interactive-graph.test.tsx | 46 ++++++ .../interactive-graph.testdata.ts | 3 +- .../locked-figures/locked-vector.tsx | 17 ++- 10 files changed, 254 insertions(+), 7 deletions(-) create mode 100644 .changeset/red-garlics-divide.md diff --git a/.changeset/red-garlics-divide.md b/.changeset/red-garlics-divide.md new file mode 100644 index 0000000000..451bdd12ec --- /dev/null +++ b/.changeset/red-garlics-divide.md @@ -0,0 +1,6 @@ +--- +"@khanacademy/perseus": minor +"@khanacademy/perseus-editor": minor +--- + +[Locked Figure Aria] Implement locked vector aria labels (graph + editor) diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-vector-settings.test.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-vector-settings.test.tsx index 6d25b30c63..8bbd3ea201 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-vector-settings.test.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-vector-settings.test.tsx @@ -366,4 +366,136 @@ describe("Locked Vector Settings", () => { }); }); }); + + 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("Vector 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 (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: "Vector 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: "Vector 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: "Vector from (0, 0) to (2, 2) with labels A, B", + }); + }); + }); }); diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-vector-settings.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-vector-settings.tsx index bbd7477071..4c9b1e2f72 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-vector-settings.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-vector-settings.tsx @@ -20,6 +20,7 @@ import PerseusEditorAccordion from "../../../components/perseus-editor-accordion import ColorSelect from "./color-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 {getDefaultFigureForType} from "./util"; @@ -49,6 +50,7 @@ const LockedVectorSettings = (props: Props) => { points, color: lineColor, labels, + ariaLabel, onChangeProps, onMove, onRemove, @@ -59,6 +61,29 @@ const LockedVectorSettings = (props: Props) => { // Check if the line has length 0. const isInvalid = kvector.equal(tail, tip); + function getPrepopulatedAriaLabel() { + let str = `Vector from (${tail[0]}, ${tail[1]}) to (${tip[0]}, ${tip[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"; + } + + for (let i = 0; i < labels.length; i++) { + // Separate additional labels with commas. + if (i > 0) { + str += ","; + } + str += ` ${labels[i].text}`; + } + } + + return str; + } + function handleChangePoint(newCoord: Coord | undefined, index: 0 | 1) { if (typeof newCoord !== "undefined") { const newPoints = [...points] satisfies [tail: Coord, tip: Coord]; @@ -187,9 +212,28 @@ const LockedVectorSettings = (props: Props) => { /> + {flags?.["mafs"]?.["locked-figures-aria"] && ( + <> + + + + { + onChangeProps(newProps); + }} + /> + + )} + {flags?.["mafs"]?.["locked-vector-labels"] && ( <> + + + + Visible labels {labels?.map((label, labelIndex) => ( { ); case "vector": return ( - + ); case "ellipse": return ( diff --git a/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.test.ts b/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.test.ts index ca00988816..ef1a69b5a7 100644 --- a/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.test.ts +++ b/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.test.ts @@ -980,6 +980,7 @@ describe("InteractiveGraphQuestionBuilder", () => { .addLockedVector([1, 2], [3, 4], { color: "green", labels: [{text: "a label"}], + ariaLabel: "an aria label", }) .build(); const graph = question.widgets["interactive-graph 1"]; @@ -1001,6 +1002,7 @@ describe("InteractiveGraphQuestionBuilder", () => { size: "medium", }, ], + ariaLabel: "an aria label", }, ]); }); diff --git a/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.ts b/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.ts index fe5b57ca50..8d0eae26a3 100644 --- a/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.ts +++ b/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.ts @@ -359,6 +359,7 @@ class InteractiveGraphQuestionBuilder { options?: { color?: LockedFigureColor; labels?: LockedFigureLabelOptions[]; + ariaLabel?: string; }, ): InteractiveGraphQuestionBuilder { const vector: LockedVectorType = { @@ -372,6 +373,7 @@ class InteractiveGraphQuestionBuilder { color: options?.color ?? "grayH", size: label.size ?? "medium", })), + ariaLabel: options?.ariaLabel, }; this.addLockedFigure(vector); return this; diff --git a/packages/perseus/src/widgets/interactive-graphs/interactive-graph.test.tsx b/packages/perseus/src/widgets/interactive-graphs/interactive-graph.test.tsx index 73c150db45..025c0cbe2d 100644 --- a/packages/perseus/src/widgets/interactive-graphs/interactive-graph.test.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/interactive-graph.test.tsx @@ -767,6 +767,52 @@ describe("locked layer", () => { ); }); + it("should render locked vector with aria label when one is provided", () => { + // Arrange + const lockedVectorWithAriaLabelQuestion = + interactiveGraphQuestionBuilder() + .addLockedVector([0, 0], [2, 2], { + ariaLabel: "Vector A", + }) + .build(); + const {container} = renderQuestion(lockedVectorWithAriaLabelQuestion, { + flags: { + mafs: { + segment: true, + "locked-figures-aria": true, + }, + }, + }); + + // Act + // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access + const point = container.querySelector(".locked-vector"); + + // Assert + expect(point).toHaveAttribute("aria-label", "Vector A"); + }); + + it("should render locked vector without aria label by default", () => { + // Arrange + const simpleLockedVectorquestion = interactiveGraphQuestionBuilder() + .addLockedVector([0, 0], [2, 2]) + .build(); + const {container} = renderQuestion(simpleLockedVectorquestion, { + flags: { + mafs: { + segment: true, + }, + }, + }); + + // Act + // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access + const point = container.querySelector(".locked-vector"); + + // Assert + expect(point).not.toHaveAttribute("aria-label"); + }); + it("should render locked ellipses", async () => { // Arrange const {container} = renderQuestion(segmentWithLockedEllipses, { diff --git a/packages/perseus/src/widgets/interactive-graphs/interactive-graph.testdata.ts b/packages/perseus/src/widgets/interactive-graphs/interactive-graph.testdata.ts index 09b66bd77f..a48b81637f 100644 --- a/packages/perseus/src/widgets/interactive-graphs/interactive-graph.testdata.ts +++ b/packages/perseus/src/widgets/interactive-graphs/interactive-graph.testdata.ts @@ -835,11 +835,12 @@ export const segmentWithLockedFigures: PerseusRenderer = showPoint1: true, showPoint2: true, labels: [{text: "B"}], - ariaLabel: "Line PQ", + ariaLabel: "Line B", }) .addLockedVector([0, 0], [8, 2], { color: "purple", labels: [{text: "C"}], + ariaLabel: "Vector C", }) .addLockedEllipse([0, 5], [4, 2], { angle: Math.PI / 4, diff --git a/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-vector.tsx b/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-vector.tsx index 76da8f769d..ec1470afbe 100644 --- a/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-vector.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-vector.tsx @@ -4,13 +4,24 @@ import {lockedFigureColors} from "../../../perseus-types"; import {Vector} from "../graphs/components/vector"; import type {LockedVectorType} from "../../../perseus-types"; +import type {APIOptions} from "../../../types"; -const LockedVector = (props: LockedVectorType) => { - const {color, points} = props; +type Props = LockedVectorType & { + flags?: APIOptions["flags"]; +}; + +const LockedVector = (props: Props) => { + const {color, points, ariaLabel, flags} = props; const [tail, tip] = points; + const hasAria = ariaLabel && flags?.["mafs"]?.["locked-figures-aria"]; + return ( - + );