Skip to content

Commit e2f2cee

Browse files
authored
[SR] Linear - Add the interactive elements linear description to the whole graph container (#2110)
## Summary: Add the interactive Linear graph description to the full graph container. This adds the "Interactive elements: Line..." description to the outermost graph container. Issue: https://khanacademy.atlassian.net/browse/LEMS-1726 ## Test plan: `yarn jest packages/perseus/src/widgets/interactive-graphs/graphs/linear.test.tsx` Storybook - Go to http://localhost:6006/iframe.html?globals=&args=&id=perseuseditor-widgets-interactive-graph--interactive-graph-linear&viewMode=story - Confirm that there is the "interactive elements: ..." section in the SR tree - Use screen reader to confirm it's read out loud Author: nishasy Reviewers: catandthemachines, anakaren-rojas Required Reviewers: Approved By: catandthemachines Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x) Pull Request URL: #2110
1 parent 117e78d commit e2f2cee

File tree

3 files changed

+136
-22
lines changed

3 files changed

+136
-22
lines changed

.changeset/lemon-apricots-shop.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@khanacademy/perseus": patch
3+
---
4+
5+
[SR] Linear - Add the interactive elements linear description to the whole graph container

packages/perseus/src/widgets/interactive-graphs/graphs/linear.test.tsx

+67-3
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ import * as React from "react";
55
import {Dependencies} from "@khanacademy/perseus";
66

77
import {testDependencies} from "../../../../../../testing/test-dependencies";
8+
import {mockPerseusI18nContext} from "../../../components/i18n-context";
89
import {MafsGraph} from "../mafs-graph";
910
import {getBaseMafsGraphPropsForTests} from "../utils";
1011

12+
import {describeLinearGraph} from "./linear";
13+
1114
import type {InteractiveGraphState} from "../types";
1215
import type {UserEvent} from "@testing-library/user-event";
1316

@@ -50,9 +53,8 @@ describe("Linear graph screen reader", () => {
5053

5154
// Assert
5255
expect(linearGraph).toBeInTheDocument();
53-
expect(linearGraph).toHaveAttribute(
54-
"aria-describedby",
55-
":r1:-points :r1:-intercept :r1:-slope",
56+
expect(linearGraph).toHaveAccessibleDescription(
57+
"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.",
5658
);
5759
});
5860

@@ -239,3 +241,65 @@ describe("Linear graph screen reader", () => {
239241
},
240242
);
241243
});
244+
245+
describe("describeLinearGraph", () => {
246+
test("describes a default linear graph", () => {
247+
// Arrange
248+
249+
// Act
250+
const strings = describeLinearGraph(
251+
baseLinearState,
252+
mockPerseusI18nContext,
253+
);
254+
255+
// Assert
256+
expect(strings.srLinearGraph).toBe("A line on a coordinate plane.");
257+
expect(strings.srLinearGraphPoints).toBe(
258+
"The line has two points, point 1 at -5 comma 5 and point 2 at 5 comma 5.",
259+
);
260+
expect(strings.srLinearGrabHandle).toBe(
261+
"Line from -5 comma 5 to 5 comma 5.",
262+
);
263+
expect(strings.slopeString).toBe("Its slope is zero.");
264+
expect(strings.interceptString).toBe(
265+
"The line crosses the Y-axis at 0 comma 5.",
266+
);
267+
expect(strings.srLinearInteractiveElement).toBe(
268+
"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.",
269+
);
270+
});
271+
272+
test("describes a linear graph with updated points", () => {
273+
// Arrange
274+
275+
// Act
276+
const strings = describeLinearGraph(
277+
{
278+
...baseLinearState,
279+
coords: [
280+
[-1, 2],
281+
[3, 4],
282+
],
283+
},
284+
mockPerseusI18nContext,
285+
);
286+
287+
// Assert
288+
expect(strings.srLinearGraph).toBe("A line on a coordinate plane.");
289+
expect(strings.srLinearGraphPoints).toBe(
290+
"The line has two points, point 1 at -1 comma 2 and point 2 at 3 comma 4.",
291+
);
292+
expect(strings.srLinearGrabHandle).toBe(
293+
"Line from -1 comma 2 to 3 comma 4.",
294+
);
295+
expect(strings.slopeString).toBe(
296+
"Its slope increases from left to right.",
297+
);
298+
expect(strings.interceptString).toBe(
299+
"The line crosses the X-axis at -5 comma 0 and the Y-axis at 0 comma 2.5.",
300+
);
301+
expect(strings.srLinearInteractiveElement).toBe(
302+
"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.",
303+
);
304+
});
305+
});

packages/perseus/src/widgets/interactive-graphs/graphs/linear.tsx

+64-19
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {MovableLine} from "./components/movable-line";
88
import {srFormatNumber} from "./screenreader-text";
99
import {getInterceptStringForLine, getSlopeStringForLine} from "./utils";
1010

11+
import type {I18nContextType} from "../../../components/i18n-context";
1112
import type {
1213
MafsGraphProps,
1314
LinearGraphState,
@@ -22,7 +23,9 @@ export function renderLinearGraph(
2223
): InteractiveGraphElementSuite {
2324
return {
2425
graph: <LinearGraph graphState={state} dispatch={dispatch} />,
25-
interactiveElementsDescription: null,
26+
interactiveElementsDescription: (
27+
<LinearGraphDescription state={state} />
28+
),
2629
};
2730
}
2831

@@ -38,34 +41,27 @@ const LinearGraph = (props: LinearGraphProps, key: number) => {
3841
const interceptDescriptionId = id + "-intercept";
3942
const slopeDescriptionId = id + "-slope";
4043

41-
// Aria label strings
42-
const linearGraphPointsDescription = strings.srLinearGraphPoints({
43-
point1X: srFormatNumber(line[0][0], locale),
44-
point1Y: srFormatNumber(line[0][1], locale),
45-
point2X: srFormatNumber(line[1][0], locale),
46-
point2Y: srFormatNumber(line[1][1], locale),
47-
});
48-
const grabHandleAriaLabel = strings.srLinearGrabHandle({
49-
point1X: srFormatNumber(line[0][0], locale),
50-
point1Y: srFormatNumber(line[0][1], locale),
51-
point2X: srFormatNumber(line[1][0], locale),
52-
point2Y: srFormatNumber(line[1][1], locale),
53-
});
54-
const slopeString = getSlopeStringForLine(line, strings);
55-
const interceptString = getInterceptStringForLine(line, strings, locale);
44+
// Aria strings
45+
const {
46+
srLinearGraph,
47+
srLinearGraphPoints,
48+
srLinearGrabHandle,
49+
slopeString,
50+
interceptString,
51+
} = describeLinearGraph(props.graphState, {strings, locale});
5652

5753
// Linear graphs only have one line
5854
// (LEMS-2050): Update the reducer so that we have a separate action for moving one line
5955
// and another action for moving multiple lines
6056
return (
6157
<g
6258
// Outer line minimal description
63-
aria-label={strings.srLinearGraph}
59+
aria-label={srLinearGraph}
6460
aria-describedby={`${pointsDescriptionId} ${interceptDescriptionId} ${slopeDescriptionId}`}
6561
>
6662
<MovableLine
6763
key={0}
68-
ariaLabels={{grabHandleAriaLabel: grabHandleAriaLabel}}
64+
ariaLabels={{grabHandleAriaLabel: srLinearGrabHandle}}
6965
ariaDescribedBy={`${interceptDescriptionId} ${slopeDescriptionId}`}
7066
points={line}
7167
onMoveLine={(delta: vec.Vector2) => {
@@ -88,7 +84,7 @@ const LinearGraph = (props: LinearGraphProps, key: number) => {
8884
{/* Hidden elements to provide the descriptions for the
8985
circle and radius point's `aria-describedby` properties. */}
9086
<g id={pointsDescriptionId} style={a11y.srOnly}>
91-
{linearGraphPointsDescription}
87+
{srLinearGraphPoints}
9288
</g>
9389
<g id={interceptDescriptionId} style={a11y.srOnly}>
9490
{interceptString}
@@ -99,3 +95,52 @@ const LinearGraph = (props: LinearGraphProps, key: number) => {
9995
</g>
10096
);
10197
};
98+
99+
function LinearGraphDescription({state}: {state: LinearGraphState}) {
100+
// The reason that LinearGraphDescription is a component (rather than a
101+
// function that returns a string) is because it needs to use a
102+
// hook: `usePerseusI18n`.
103+
const i18n = usePerseusI18n();
104+
const strings = describeLinearGraph(state, i18n);
105+
106+
return strings.srLinearInteractiveElement;
107+
}
108+
109+
// Exported for testing
110+
export function describeLinearGraph(
111+
state: LinearGraphState,
112+
i18n: I18nContextType,
113+
): Record<string, string> {
114+
const {coords: line} = state;
115+
const {strings, locale} = i18n;
116+
117+
// Aria label strings
118+
const srLinearGraph = strings.srLinearGraph;
119+
const srLinearGraphPoints = strings.srLinearGraphPoints({
120+
point1X: srFormatNumber(line[0][0], locale),
121+
point1Y: srFormatNumber(line[0][1], locale),
122+
point2X: srFormatNumber(line[1][0], locale),
123+
point2Y: srFormatNumber(line[1][1], locale),
124+
});
125+
const srLinearGrabHandle = strings.srLinearGrabHandle({
126+
point1X: srFormatNumber(line[0][0], locale),
127+
point1Y: srFormatNumber(line[0][1], locale),
128+
point2X: srFormatNumber(line[1][0], locale),
129+
point2Y: srFormatNumber(line[1][1], locale),
130+
});
131+
const slopeString = getSlopeStringForLine(line, strings);
132+
const interceptString = getInterceptStringForLine(line, strings, locale);
133+
134+
const srLinearInteractiveElement = strings.srInteractiveElements({
135+
elements: [srLinearGraph, srLinearGraphPoints].join(" "),
136+
});
137+
138+
return {
139+
srLinearGraph,
140+
srLinearGraphPoints,
141+
srLinearGrabHandle,
142+
slopeString,
143+
interceptString,
144+
srLinearInteractiveElement,
145+
};
146+
}

0 commit comments

Comments
 (0)