Skip to content

Commit

Permalink
[SR] Circle - Add the interactive elements circle description to the …
Browse files Browse the repository at this point in the history
…whole graph container (#2060)

## Summary:
Add the interactive Circle graph description to the full graph container.

This adds the "Interactive elements: Circle..." description to the outermost graph container.

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

## Test plan:
- Go to https://650db21c3f5d1b2f13c02952-iupnsmdbhn.chromatic.com/iframe.html?globals=&args=&id=perseuseditor-widgets-interactive-graph--interactive-graph-circle&viewMode=story
- Use a screen reader to get to the outermost graph container
- Confirm that it has the circle description after "Interactive elements:"

Author: nishasy

Reviewers: anakaren-rojas, nishasy, benchristel, catandthemachines

Required Reviewers:

Approved By: benchristel

Checks: ✅ Cypress (ubuntu-latest, 20.x), ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Check builds for changes in size (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), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x)

Pull Request URL: #2060
  • Loading branch information
nishasy authored Jan 9, 2025
1 parent e23647a commit 43e99d2
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 29 deletions.
5 changes: 5 additions & 0 deletions .changeset/little-beds-tickle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus": patch
---

[SR] Circle - Add interactive Circle element to full graph description
2 changes: 1 addition & 1 deletion packages/perseus/src/components/i18n-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {mockStrings} from "../strings";

import type {PerseusStrings} from "../strings";

type I18nContextType = {
export type I18nContextType = {
strings: PerseusStrings;
locale: string;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {describeCircleGraph} from "./circle";

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

Expand Down Expand Up @@ -163,3 +166,57 @@ describe("Circle graph screen reader", () => {
expect(radiusPoint).toHaveAttribute("aria-live", "off");
});
});

describe("describeCircleGraph", () => {
test("describes a default circle", () => {
// Arrange

// Act
const strings = describeCircleGraph(
baseCircleState,
mockPerseusI18nContext,
);

// Assert
expect(strings.srCircleGraph).toBe("A circle on a coordinate plane.");
expect(strings.srCircleShape).toBe(
"Circle. The center point is at 0 comma 0.",
);
expect(strings.srCircleRadiusPoint).toBe("Radius point at 1 comma 0.");
expect(strings.srCircleRadius).toBe("Circle radius is 1.");
expect(strings.srCircleOuterPoints).toBe(
"Points on the circle at 1 comma 0, 0 comma 1, -1 comma 0, 0 comma -1.",
);
expect(strings.srCircleInteractiveElement).toBe(
"Interactive elements: Circle. The center point is at 0 comma 0. Circle radius is 1.",
);
});

test("describes a circle with updated values", () => {
// Arrange

// Act
const strings = describeCircleGraph(
{
...baseCircleState,
center: [2, 3],
radiusPoint: [7, 3],
},
mockPerseusI18nContext,
);

// Assert
expect(strings.srCircleGraph).toBe("A circle on a coordinate plane.");
expect(strings.srCircleShape).toBe(
"Circle. The center point is at 2 comma 3.",
);
expect(strings.srCircleRadiusPoint).toBe("Radius point at 7 comma 3.");
expect(strings.srCircleRadius).toBe("Circle radius is 5.");
expect(strings.srCircleOuterPoints).toBe(
"Points on the circle at 7 comma 3, 2 comma 8, -3 comma 3, 2 comma -2.",
);
expect(strings.srCircleInteractiveElement).toBe(
"Interactive elements: Circle. The center point is at 2 comma 3. Circle radius is 5.",
);
});
});
101 changes: 73 additions & 28 deletions packages/perseus/src/widgets/interactive-graphs/graphs/circle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
useTransformVectorsToPixels,
} from "./use-transform";

import type {I18nContextType} from "../../../components/i18n-context";
import type {
AriaLive,
CircleGraphState,
Expand All @@ -30,7 +31,9 @@ export function renderCircleGraph(
): InteractiveGraphElementSuite {
return {
graph: <CircleGraph graphState={state} dispatch={dispatch} />,
interactiveElementsDescription: null,
interactiveElementsDescription: (
<CircleGraphDescription state={state} />
),
};
}

Expand All @@ -51,40 +54,25 @@ function CircleGraph(props: CircleGraphProps) {
const outerPointsId = id + "-outer-points";

// Aria label strings
const circleGraphAriaLabel = strings.srCircleGraph;
const circleShapeAriaLabel = strings.srCircleShape({
centerX: srFormatNumber(center[0], locale),
centerY: srFormatNumber(center[1], locale),
});
const circleRadiusPointAriaLabel = strings.srCircleRadiusPoint({
radiusPointX: srFormatNumber(radiusPoint[0], locale),
radiusPointY: srFormatNumber(radiusPoint[1], locale),
});
const circleRadiusDescription = strings.srCircleRadius({
radius,
});
const circleOuterPointsDescription = strings.srCircleOuterPoints({
point1X: srFormatNumber(center[0] + radius, locale),
point1Y: srFormatNumber(center[1], locale),
point2X: srFormatNumber(center[0], locale),
point2Y: srFormatNumber(center[1] + radius, locale),
point3X: srFormatNumber(center[0] - radius, locale),
point3Y: srFormatNumber(center[1], locale),
point4X: srFormatNumber(center[0], locale),
point4Y: srFormatNumber(center[1] - radius, locale),
});
const {
srCircleGraph,
srCircleShape,
srCircleRadiusPoint,
srCircleRadius,
srCircleOuterPoints,
} = describeCircleGraph(graphState, {strings, locale});

return (
<g
// Outer circle minimal description
aria-label={circleGraphAriaLabel}
aria-label={srCircleGraph}
aria-describedby={`${circleId} ${radiusId} ${outerPointsId}`}
>
<MovableCircle
id={circleId}
// Focusable circle aria label reads with every update
// because of the aria-live property in the circle <g>.
ariaLabel={circleShapeAriaLabel}
ariaLabel={srCircleShape}
// Aria-describedby describes additional info on focus.
ariaDescribedBy={`${radiusId} ${outerPointsId}`}
center={center}
Expand All @@ -96,7 +84,7 @@ function CircleGraph(props: CircleGraphProps) {
/>
<MovablePoint
// Radius point aria label reads with every update.
ariaLabel={`${circleRadiusPointAriaLabel} ${circleRadiusDescription}`}
ariaLabel={`${srCircleRadiusPoint} ${srCircleRadius}`}
// Aria-describedby describes additional info on focus.
ariaDescribedBy={`${outerPointsId}`}
// The radius point's aria-live property is set to "off" when
Expand All @@ -116,10 +104,10 @@ function CircleGraph(props: CircleGraphProps) {
{/* Hidden elements to provide the descriptions for the
circle and radius point's `aria-describedby` properties. */}
<g id={radiusId} style={{display: "hidden"}}>
{circleRadiusDescription}
{srCircleRadius}
</g>
<g id={outerPointsId} style={{display: "hidden"}}>
{circleOuterPointsDescription}
{srCircleOuterPoints}
</g>
</g>
);
Expand Down Expand Up @@ -221,3 +209,60 @@ function crossProduct<A, B>(as: A[], bs: B[]): [A, B][] {
}
return result;
}

function CircleGraphDescription({state}: {state: CircleGraphState}) {
// The reason that CircleGraphDescription 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 = describeCircleGraph(state, i18n);

return strings.srCircleInteractiveElement;
}

// Exported for testing
export function describeCircleGraph(
state: CircleGraphState,
i18n: I18nContextType,
): Record<string, string> {
const {strings, locale} = i18n;
const {center, radiusPoint} = state;
const radius = getRadius(state);

// Aria label strings
const srCircleGraph = strings.srCircleGraph;
const srCircleShape = strings.srCircleShape({
centerX: srFormatNumber(center[0], locale),
centerY: srFormatNumber(center[1], locale),
});
const srCircleRadiusPoint = strings.srCircleRadiusPoint({
radiusPointX: srFormatNumber(radiusPoint[0], locale),
radiusPointY: srFormatNumber(radiusPoint[1], locale),
});
const srCircleRadius = strings.srCircleRadius({
radius,
});
const srCircleOuterPoints = strings.srCircleOuterPoints({
point1X: srFormatNumber(center[0] + radius, locale),
point1Y: srFormatNumber(center[1], locale),
point2X: srFormatNumber(center[0], locale),
point2Y: srFormatNumber(center[1] + radius, locale),
point3X: srFormatNumber(center[0] - radius, locale),
point3Y: srFormatNumber(center[1], locale),
point4X: srFormatNumber(center[0], locale),
point4Y: srFormatNumber(center[1] - radius, locale),
});

const srCircleInteractiveElement = strings.srInteractiveElements({
elements: [srCircleShape, srCircleRadius].join(" "),
});

return {
srCircleGraph,
srCircleShape,
srCircleRadiusPoint,
srCircleRadius,
srCircleOuterPoints,
srCircleInteractiveElement,
};
}

0 comments on commit 43e99d2

Please sign in to comment.