diff --git a/.changeset/lemon-apricots-shop.md b/.changeset/lemon-apricots-shop.md
new file mode 100644
index 0000000000..06121cace5
--- /dev/null
+++ b/.changeset/lemon-apricots-shop.md
@@ -0,0 +1,5 @@
+---
+"@khanacademy/perseus": patch
+---
+
+[SR] Linear - Add the interactive elements linear description to the whole graph container
diff --git a/packages/perseus/src/widgets/interactive-graphs/graphs/linear.test.tsx b/packages/perseus/src/widgets/interactive-graphs/graphs/linear.test.tsx
index 68b87e4a48..11c4249058 100644
--- a/packages/perseus/src/widgets/interactive-graphs/graphs/linear.test.tsx
+++ b/packages/perseus/src/widgets/interactive-graphs/graphs/linear.test.tsx
@@ -5,9 +5,12 @@ import * as React from "react";
import {Dependencies} from "@khanacademy/perseus";
import {testDependencies} from "../../../../../../testing/test-dependencies";
+import {mockPerseusI18nContext} from "../../../components/i18n-context";
import {MafsGraph} from "../mafs-graph";
import {getBaseMafsGraphPropsForTests} from "../utils";
+import {describeLinearGraph} from "./linear";
+
import type {InteractiveGraphState} from "../types";
import type {UserEvent} from "@testing-library/user-event";
@@ -50,9 +53,8 @@ describe("Linear graph screen reader", () => {
// Assert
expect(linearGraph).toBeInTheDocument();
- expect(linearGraph).toHaveAttribute(
- "aria-describedby",
- ":r1:-points :r1:-intercept :r1:-slope",
+ expect(linearGraph).toHaveAccessibleDescription(
+ "The line has two points, point 1 at -5 comma 5 and point 2 at 5 comma 5. The line crosses the Y-axis at 0 comma 5. Its slope is zero.",
);
});
@@ -239,3 +241,65 @@ describe("Linear graph screen reader", () => {
},
);
});
+
+describe("describeLinearGraph", () => {
+ test("describes a default linear graph", () => {
+ // Arrange
+
+ // Act
+ const strings = describeLinearGraph(
+ baseLinearState,
+ mockPerseusI18nContext,
+ );
+
+ // Assert
+ expect(strings.srLinearGraph).toBe("A line on a coordinate plane.");
+ expect(strings.srLinearGraphPoints).toBe(
+ "The line has two points, point 1 at -5 comma 5 and point 2 at 5 comma 5.",
+ );
+ expect(strings.srLinearGrabHandle).toBe(
+ "Line from -5 comma 5 to 5 comma 5.",
+ );
+ expect(strings.slopeString).toBe("Its slope is zero.");
+ expect(strings.interceptString).toBe(
+ "The line crosses the Y-axis at 0 comma 5.",
+ );
+ expect(strings.srLinearInteractiveElement).toBe(
+ "Interactive elements: A line on a coordinate plane. The line has two points, point 1 at -5 comma 5 and point 2 at 5 comma 5.",
+ );
+ });
+
+ test("describes a linear graph with updated points", () => {
+ // Arrange
+
+ // Act
+ const strings = describeLinearGraph(
+ {
+ ...baseLinearState,
+ coords: [
+ [-1, 2],
+ [3, 4],
+ ],
+ },
+ mockPerseusI18nContext,
+ );
+
+ // Assert
+ expect(strings.srLinearGraph).toBe("A line on a coordinate plane.");
+ expect(strings.srLinearGraphPoints).toBe(
+ "The line has two points, point 1 at -1 comma 2 and point 2 at 3 comma 4.",
+ );
+ expect(strings.srLinearGrabHandle).toBe(
+ "Line from -1 comma 2 to 3 comma 4.",
+ );
+ expect(strings.slopeString).toBe(
+ "Its slope increases from left to right.",
+ );
+ expect(strings.interceptString).toBe(
+ "The line crosses the X-axis at -5 comma 0 and the Y-axis at 0 comma 2.5.",
+ );
+ expect(strings.srLinearInteractiveElement).toBe(
+ "Interactive elements: A line on a coordinate plane. The line has two points, point 1 at -1 comma 2 and point 2 at 3 comma 4.",
+ );
+ });
+});
diff --git a/packages/perseus/src/widgets/interactive-graphs/graphs/linear.tsx b/packages/perseus/src/widgets/interactive-graphs/graphs/linear.tsx
index f0b0f429df..85f26ca8ab 100644
--- a/packages/perseus/src/widgets/interactive-graphs/graphs/linear.tsx
+++ b/packages/perseus/src/widgets/interactive-graphs/graphs/linear.tsx
@@ -8,6 +8,7 @@ import {MovableLine} from "./components/movable-line";
import {srFormatNumber} from "./screenreader-text";
import {getInterceptStringForLine, getSlopeStringForLine} from "./utils";
+import type {I18nContextType} from "../../../components/i18n-context";
import type {
MafsGraphProps,
LinearGraphState,
@@ -22,7 +23,9 @@ export function renderLinearGraph(
): InteractiveGraphElementSuite {
return {
graph: ,
- interactiveElementsDescription: null,
+ interactiveElementsDescription: (
+
+ ),
};
}
@@ -38,21 +41,14 @@ const LinearGraph = (props: LinearGraphProps, key: number) => {
const interceptDescriptionId = id + "-intercept";
const slopeDescriptionId = id + "-slope";
- // Aria label strings
- const linearGraphPointsDescription = strings.srLinearGraphPoints({
- point1X: srFormatNumber(line[0][0], locale),
- point1Y: srFormatNumber(line[0][1], locale),
- point2X: srFormatNumber(line[1][0], locale),
- point2Y: srFormatNumber(line[1][1], locale),
- });
- const grabHandleAriaLabel = strings.srLinearGrabHandle({
- point1X: srFormatNumber(line[0][0], locale),
- point1Y: srFormatNumber(line[0][1], locale),
- point2X: srFormatNumber(line[1][0], locale),
- point2Y: srFormatNumber(line[1][1], locale),
- });
- const slopeString = getSlopeStringForLine(line, strings);
- const interceptString = getInterceptStringForLine(line, strings, locale);
+ // Aria strings
+ const {
+ srLinearGraph,
+ srLinearGraphPoints,
+ srLinearGrabHandle,
+ slopeString,
+ interceptString,
+ } = describeLinearGraph(props.graphState, {strings, locale});
// Linear graphs only have one line
// (LEMS-2050): Update the reducer so that we have a separate action for moving one line
@@ -60,12 +56,12 @@ const LinearGraph = (props: LinearGraphProps, key: number) => {
return (
{
@@ -88,7 +84,7 @@ const LinearGraph = (props: LinearGraphProps, key: number) => {
{/* Hidden elements to provide the descriptions for the
circle and radius point's `aria-describedby` properties. */}
- {linearGraphPointsDescription}
+ {srLinearGraphPoints}
{interceptString}
@@ -99,3 +95,52 @@ const LinearGraph = (props: LinearGraphProps, key: number) => {
);
};
+
+function LinearGraphDescription({state}: {state: LinearGraphState}) {
+ // The reason that LinearGraphDescription is a component (rather than a
+ // function that returns a string) is because it needs to use a
+ // hook: `usePerseusI18n`.
+ const i18n = usePerseusI18n();
+ const strings = describeLinearGraph(state, i18n);
+
+ return strings.srLinearInteractiveElement;
+}
+
+// Exported for testing
+export function describeLinearGraph(
+ state: LinearGraphState,
+ i18n: I18nContextType,
+): Record {
+ const {coords: line} = state;
+ const {strings, locale} = i18n;
+
+ // Aria label strings
+ const srLinearGraph = strings.srLinearGraph;
+ const srLinearGraphPoints = strings.srLinearGraphPoints({
+ point1X: srFormatNumber(line[0][0], locale),
+ point1Y: srFormatNumber(line[0][1], locale),
+ point2X: srFormatNumber(line[1][0], locale),
+ point2Y: srFormatNumber(line[1][1], locale),
+ });
+ const srLinearGrabHandle = strings.srLinearGrabHandle({
+ point1X: srFormatNumber(line[0][0], locale),
+ point1Y: srFormatNumber(line[0][1], locale),
+ point2X: srFormatNumber(line[1][0], locale),
+ point2Y: srFormatNumber(line[1][1], locale),
+ });
+ const slopeString = getSlopeStringForLine(line, strings);
+ const interceptString = getInterceptStringForLine(line, strings, locale);
+
+ const srLinearInteractiveElement = strings.srInteractiveElements({
+ elements: [srLinearGraph, srLinearGraphPoints].join(" "),
+ });
+
+ return {
+ srLinearGraph,
+ srLinearGraphPoints,
+ srLinearGrabHandle,
+ slopeString,
+ interceptString,
+ srLinearInteractiveElement,
+ };
+}