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, + }; +}