Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SR] Ray graph - Add screen reader support for Ray interactive graph #2036

Merged
merged 39 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
30333ff
[SR] Linear graph - add grab handle description and aria lives
nishasy Dec 18, 2024
586c962
docs(changeset): [SR] Linear graph - add grab handle description and …
nishasy Dec 18, 2024
ee8b9b2
Merge branch 'main' into sr-linear-body
nishasy Dec 18, 2024
a5ef0f8
Linear System SR
nishasy Dec 18, 2024
0fbbbfa
docs(changeset): [SR] Linear System - add screen reader support for L…
nishasy Dec 18, 2024
48f17c5
Ray SR
nishasy Dec 18, 2024
625c9a4
docs(changeset): [SR] Ray graph - Add screen reader support for Ray i…
nishasy Dec 18, 2024
951fc96
Merge branch 'main' into sr-linear-system
nishasy Dec 19, 2024
193ee2a
fix misunderstanding from linear PR. update tests
nishasy Dec 19, 2024
1ffe7fa
Merge branch 'sr-linear-system' into sr-ray
nishasy Dec 19, 2024
f754610
update test
nishasy Dec 19, 2024
c01b4f6
Merge branch 'main' into sr-linear-system
nishasy Jan 8, 2025
b315fbf
remove contexts from strings
nishasy Jan 8, 2025
7d96885
Add full graph description of all interactive elements
nishasy Jan 8, 2025
8c8cbf5
Merge branch 'sr-linear-system' into sr-ray
nishasy Jan 9, 2025
33e209a
remove context from strings
nishasy Jan 9, 2025
914ea5a
Add full graph description of interactive element
nishasy Jan 9, 2025
9ac9782
Rename lineSequence --> lineNumber
nishasy Jan 9, 2025
c242323
Merge branch 'sr-linear-system' into sr-ray
nishasy Jan 9, 2025
5b48e40
Create custom matcher for aria descriptions
nishasy Jan 9, 2025
24f71a1
Include ID in template string
nishasy Jan 9, 2025
8669b75
Use srOnly style instead of invalid display hidden
nishasy Jan 9, 2025
ed8ccbd
use isFinite
nishasy Jan 9, 2025
4523ca0
Merge branch 'main' into sr-linear-system
nishasy Jan 9, 2025
07b60bf
Merge branch 'sr-linear-system' into sr-ray
nishasy Jan 9, 2025
e625ea6
use srOnly style
nishasy Jan 9, 2025
c56b3c3
use new matcher for aria descriptions in test
nishasy Jan 9, 2025
f8c8c11
handle SR for lines overlapping axes
nishasy Jan 15, 2025
1dcf3fd
Use existing aria description matcher rather than creating a new one
nishasy Jan 15, 2025
d64ac0d
correct string in test
nishasy Jan 15, 2025
a2210a5
Merge branch 'sr-linear-system' into sr-ray
nishasy Jan 15, 2025
91f3166
Fix comments
nishasy Jan 15, 2025
4468c3e
Add types
nishasy Jan 15, 2025
cbd82f6
Merge branch 'sr-linear-system' into sr-ray
nishasy Jan 15, 2025
e4a7322
fix import
nishasy Jan 15, 2025
11f2a4a
Update grab handle description to match SRUX doc
nishasy Jan 15, 2025
336ac2f
Merge branch 'sr-linear-system' into sr-ray
nishasy Jan 15, 2025
a164397
Update copy to match SRUX doc
nishasy Jan 15, 2025
e5b8cd1
Merge branch 'main' into sr-ray
nishasy Jan 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading