Skip to content

Commit

Permalink
[SR] Ray graph - Add screen reader support for Ray interactive graph (#…
Browse files Browse the repository at this point in the history
…2036)

## Summary:
Add the aria label and descriptions for the full graph and the
interactive elements in the Linear System graph, based on the
[SRUX doc](https://khanacademy.atlassian.net/wiki/spaces/LC/pages/3460366337/Ray).

Issue: https://khanacademy.atlassian.net/browse/LEMS-1734

## Test plan:
`yarn jest packages/perseus/src/widgets/interactive-graphs/graphs/ray.test.tsx`

Storybook
http://localhost:6006/iframe.html?globals=&args=&id=perseuseditor-widgets-interactive-graph--interactive-graph-ray&viewMode=story


https://github.com/user-attachments/assets/fd00be9c-a8a6-42ca-af44-6f4f2bd1a0d3

Author: nishasy

Reviewers: catandthemachines, anakaren-rojas, nishasy

Required Reviewers:

Approved By: catandthemachines, anakaren-rojas

Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Cypress (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: #2036
  • Loading branch information
nishasy authored Jan 16, 2025
1 parent d96821e commit 0f8d11c
Show file tree
Hide file tree
Showing 5 changed files with 356 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/wild-keys-sit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus": patch
---

[SR] Ray graph - Add screen reader support for Ray interactive graph
39 changes: 39 additions & 0 deletions packages/perseus/src/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,31 @@ export type PerseusStrings = {
x: string;
y: string;
}): string;
srRayGraph: string;
srRayPoints: ({
point1X,
point1Y,
point2X,
point2Y,
}: {
point1X: string;
point1Y: string;
point2X: string;
point2Y: string;
}) => string;
srRayEndpoint: ({x, y}: {x: string; y: string}) => string;
srRayTerminalPoint: ({x, y}: {x: string; y: string}) => string;
srRayGrabHandle: ({
point1X,
point1Y,
point2X,
point2Y,
}: {
point1X: string;
point1Y: string;
point2X: string;
point2Y: string;
}) => string;
// The above strings are used for interactive graph SR descriptions.
};

Expand Down Expand Up @@ -510,6 +535,13 @@ export const strings: {
"Line %(lineNumber)s has two points, point 1 at %(point1X)s comma %(point1Y)s and point 2 at %(point2X)s comma %(point2Y)s.",
srLinearSystemPoint:
"Point %(pointSequence)s on line %(lineNumber)s at %(x)s comma %(y)s.",
srRayGraph: "A ray on a coordinate plane.",
srRayPoints:
"The endpoint is at %(point1X)s comma %(point1Y)s and the ray goes through point %(point2X)s comma %(point2Y)s.",
srRayGrabHandle:
"Ray with endpoint %(point1X)s comma %(point1Y)s going through point %(point2X)s comma %(point2Y)s.",
srRayEndpoint: "Endpoint at %(point1X)s comma %(point1Y)s.",
srRayTerminalPoint: "Through point at %(point2X)s comma %(point2Y)s.",
// The above strings are used for interactive graph SR descriptions.
};

Expand Down Expand Up @@ -732,6 +764,13 @@ export const mockStrings: PerseusStrings = {
`Line ${lineNumber} has two points, point 1 at ${point1X} comma ${point1Y} and point 2 at ${point2X} comma ${point2Y}.`,
srLinearSystemPoint: ({lineNumber, pointSequence, x, y}) =>
`Point ${pointSequence} on line ${lineNumber} at ${x} comma ${y}.`,
srRayGraph: "A ray on a coordinate plane.",
srRayPoints: ({point1X, point1Y, point2X, point2Y}) =>
`The endpoint is at ${point1X} comma ${point1Y} and the ray goes through point ${point2X} comma ${point2Y}.`,
srRayGrabHandle: ({point1X, point1Y, point2X, point2Y}) =>
`Ray with endpoint ${point1X} comma ${point1Y} going through point ${point2X} comma ${point2Y}.`,
srRayEndpoint: ({x, y}) => `Endpoint at ${x} comma ${y}.`,
srRayTerminalPoint: ({x, y}) => `Through point at ${x} comma ${y}.`,
// The above strings are used for interactive graph SR descriptions.
};

Expand Down
212 changes: 212 additions & 0 deletions packages/perseus/src/widgets/interactive-graphs/graphs/ray.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import {render, screen} from "@testing-library/react";
import {userEvent as userEventLib} from "@testing-library/user-event";
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 {describeRayGraph} from "./ray";

import type {InteractiveGraphState} from "../types";
import type {UserEvent} from "@testing-library/user-event";

const baseMafsGraphProps = getBaseMafsGraphPropsForTests();
const baseRayState: InteractiveGraphState = {
type: "ray",
coords: [
[-5, 5],
[5, 5],
],
hasBeenInteractedWith: false,
range: [
[-10, 10],
[-10, 10],
],
snapStep: [1, 1],
};

const overallGraphLabel = "A ray on a coordinate plane.";

describe("Linear graph screen reader", () => {
let userEvent: UserEvent;
beforeEach(() => {
userEvent = userEventLib.setup({
advanceTimers: jest.advanceTimersByTime,
});
jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
testDependencies,
);
});

test("should have aria label and describedby for overall linear graph", () => {
// Arrange
render(<MafsGraph {...baseMafsGraphProps} state={baseRayState} />);

// Act
const linearGraph = screen.getByLabelText(
"A ray on a coordinate plane.",
);

// Assert
expect(linearGraph).toBeInTheDocument();
expect(linearGraph).toHaveAccessibleDescription(
"The endpoint is at -5 comma 5 and the ray goes through point 5 comma 5.",
);
});

test.each`
element | index | expectedValue
${"point1"} | ${0} | ${"Endpoint at -5 comma 5."}
${"grabHandle"} | ${1} | ${"Ray with endpoint -5 comma 5 going through point 5 comma 5."}
${"point2"} | ${2} | ${"Through point at 5 comma 5."}
`(
"should have aria label for $element on the line",
({index, expectedValue}) => {
// Arrange
render(<MafsGraph {...baseMafsGraphProps} state={baseRayState} />);

// Act
// Moveable elements: point 1, grab handle, point 2
const movableElements = screen.getAllByRole("button");
const element = movableElements[index];

// Assert
expect(element).toHaveAttribute("aria-label", expectedValue);
},
);

test("points description should include points info", () => {
// Arrange
render(<MafsGraph {...baseMafsGraphProps} state={baseRayState} />);

// Act
const linearGraph = screen.getByLabelText(overallGraphLabel);

// Assert
expect(linearGraph).toHaveTextContent(
"The endpoint is at -5 comma 5 and the ray goes through point 5 comma 5.",
);
});

test("aria label reflects updated values", async () => {
// Arrange

// Act
render(
<MafsGraph
{...baseMafsGraphProps}
state={{
...baseRayState,
// Different points than default (-5, 5) and (5, 5)
coords: [
[-2, 3],
[3, 3],
],
}}
/>,
);

const interactiveElements = screen.getAllByRole("button");
const [point1, grabHandle, point2] = interactiveElements;

// Assert
// Check updated aria-label for the linear graph.
expect(point1).toHaveAttribute("aria-label", "Endpoint at -2 comma 3.");
expect(grabHandle).toHaveAttribute(
"aria-label",
"Ray with endpoint -2 comma 3 going through point 3 comma 3.",
);
expect(point2).toHaveAttribute(
"aria-label",
"Through point at 3 comma 3.",
);
});

test.each`
elementName | index
${"point1"} | ${0}
${"grabHandle"} | ${1}
${"point2"} | ${2}
`(
"Should update the aria-live when $elementName is moved",
async ({index}) => {
// Arrange
render(<MafsGraph {...baseMafsGraphProps} state={baseRayState} />);
const interactiveElements = screen.getAllByRole("button");
const [point1, grabHandle, point2] = interactiveElements;
const movingElement = interactiveElements[index];

// Act - Move the element
movingElement.focus();
await userEvent.keyboard("{ArrowRight}");

const expectedAriaLive = ["off", "off", "off"];
expectedAriaLive[index] = "polite";

// Assert
expect(point1).toHaveAttribute("aria-live", expectedAriaLive[0]);
expect(grabHandle).toHaveAttribute(
"aria-live",
expectedAriaLive[1],
);
expect(point2).toHaveAttribute("aria-live", expectedAriaLive[2]);
},
);
});

describe("describeRayGraph", () => {
test("describes a default ray", () => {
// Arrange

// Act
const strings = describeRayGraph(baseRayState, mockPerseusI18nContext);

// Assert
expect(strings.srRayGraph).toBe("A ray on a coordinate plane.");
expect(strings.srRayPoints).toBe(
"The endpoint is at -5 comma 5 and the ray goes through point 5 comma 5.",
);
expect(strings.srRayEndpoint).toBe("Endpoint at -5 comma 5.");
expect(strings.srRayTerminalPoint).toBe("Through point at 5 comma 5.");
expect(strings.srRayGrabHandle).toBe(
"Ray with endpoint -5 comma 5 going through point 5 comma 5.",
);
expect(strings.srRayInteractiveElement).toBe(
"Interactive elements: A ray on a coordinate plane. The endpoint is at -5 comma 5 and the ray goes through point 5 comma 5.",
);
});

test("describes a ray with updated points", () => {
// Arrange

// Act
const strings = describeRayGraph(
{
...baseRayState,
coords: [
[-1, 2],
[3, 4],
],
},
mockPerseusI18nContext,
);

// Assert
expect(strings.srRayGraph).toBe("A ray on a coordinate plane.");
expect(strings.srRayPoints).toBe(
"The endpoint is at -1 comma 2 and the ray goes through point 3 comma 4.",
);
expect(strings.srRayEndpoint).toBe("Endpoint at -1 comma 2.");
expect(strings.srRayTerminalPoint).toBe("Through point at 3 comma 4.");
expect(strings.srRayGrabHandle).toBe(
"Ray with endpoint -1 comma 2 going through point 3 comma 4.",
);
expect(strings.srRayInteractiveElement).toBe(
"Interactive elements: A ray on a coordinate plane. The endpoint is at -1 comma 2 and the ray goes through point 3 comma 4.",
);
});
});
Loading

0 comments on commit 0f8d11c

Please sign in to comment.