diff --git a/.changeset/fuzzy-cheetahs-develop.md b/.changeset/fuzzy-cheetahs-develop.md new file mode 100644 index 0000000000..392b529878 --- /dev/null +++ b/.changeset/fuzzy-cheetahs-develop.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": minor +--- + +adds aria labels to line segment diff --git a/packages/perseus/src/strings.ts b/packages/perseus/src/strings.ts index 725bdaa858..f0e1d2e774 100644 --- a/packages/perseus/src/strings.ts +++ b/packages/perseus/src/strings.ts @@ -258,6 +258,48 @@ export type PerseusStrings = { endingSideX: string; endingSideY: string; }) => string; + srSingleSegmentGraphAriaLabel: string; + srMultipleSegmentGraphAriaLabel: ({ + countOfSegments, + }: { + countOfSegments: number; + }) => string; + srIndividualSegmentAriaLabel: string; + srIndividualSegmentAriaDescription: ({ + point1X, + point1Y, + point2X, + point2Y, + length, + indexOfSegment, + }: { + point1X: string; + point1Y: string; + point2X: string; + point2Y: string; + length: string; + indexOfSegment: number; + }) => string; + srSingleSegmentGraphEndpointAriaLabel: ({ + endpointNumber, + x, + y, + }: { + endpointNumber: number; + x: string; + y: string; + }) => string; + srMultipleSegmentGraphEndpointAriaLabel: ({ + endpointNumber, + x, + y, + indexOfSegment, + }: { + endpointNumber: number; + x: string; + y: string; + indexOfSegment: number; + }) => string; // The above strings are used for interactive graph SR descriptions. }; @@ -478,6 +520,16 @@ export const strings: { srAngleGraphAriaLabel: "An angle on a coordinate plane.", srAngleGraphAriaDescription: "The angle measure is %(angleMeasure)s degrees with a vertex at %(vertexX)s comma %(vertexY)s, a point on the starting side at %(startingSideX)s comma %(startingSideY)s and a point on the ending side at %(endingSideX)s comma %(endingSideY)s", + srSingleSegmentGraphAriaLabel: "A line segment on a coordinate plane.", + srMultipleSegmentGraphAriaLabel: + "%(countOfSegments)s segments on a coordinate plane.", + srIndividualSegmentAriaLabel: "PLACEHOLDER: PLEASE UPDATE ME", + srIndividualSegmentAriaDescription: + "Segment %(indexOfSegment)s: Endpoint 1 at %(point1X)s comma %(point1Y)s. Endpoint 2 %(point2X)s comma %(point2Y)s. Segment length %(length)s units.", + srSingleSegmentGraphEndpointAriaLabel: + "Endpoint %(endpointNumber)s at %(x)s comma %(y)s.", + srMultipleSegmentGraphEndpointAriaLabel: + "Endpoint %(endpointNumber)s on segment %(indexOfSegment)s at %(x)s comma %(y)s.", // The above strings are used for interactive graph SR descriptions. }; @@ -695,5 +747,27 @@ export const mockStrings: PerseusStrings = { endingSideY, }) => `The angle measure is ${angleMeasure} degrees with a vertex at ${vertexX} comma ${vertexY}, a point on the starting side at ${startingSideX} comma ${startingSideY} and a point on the ending side at ${endingSideX} comma ${endingSideY}.`, + srSingleSegmentGraphAriaLabel: "A line segment on a coordinate plane.", + srMultipleSegmentGraphAriaLabel: ({countOfSegments}) => + `${countOfSegments} segments on a coordinate plane.`, + srIndividualSegmentAriaLabel: "PLACEHOLDER: PLEASE UPDATE ME", + srIndividualSegmentAriaDescription: ({ + point1X, + point1Y, + point2X, + point2Y, + length, + indexOfSegment, + }) => + `Segment ${indexOfSegment}: Endpoint 1 at ${point1X} comma ${point1Y}. Endpoint 2 at ${point2X} comma ${point2Y}. Segment length ${length} units.`, + srSingleSegmentGraphEndpointAriaLabel: ({endpointNumber, x, y}) => + `Endpoint ${endpointNumber} at ${x} comma ${y}.`, + srMultipleSegmentGraphEndpointAriaLabel: ({ + endpointNumber, + x, + y, + indexOfSegment, + }) => + `Endpoint ${endpointNumber} on segment ${indexOfSegment} at ${x} comma ${y}.`, // The above strings are used for interactive graph SR descriptions. }; diff --git a/packages/perseus/src/widgets/interactive-graphs/graphs/segment.tsx b/packages/perseus/src/widgets/interactive-graphs/graphs/segment.tsx index b8e5b964d1..3bfb9a86d2 100644 --- a/packages/perseus/src/widgets/interactive-graphs/graphs/segment.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/graphs/segment.tsx @@ -1,13 +1,18 @@ +import {point as kpoint} from "@khanacademy/kmath"; import * as React from "react"; +import {usePerseusI18n} from "../../../components/i18n-context"; +import {X, Y} from "../math"; import {actions} from "../reducer/interactive-graph-action"; import {MovableLine} from "./components/movable-line"; +import {srFormatNumber} from "./screenreader-text"; import type { Dispatch, InteractiveGraphElementSuite, MafsGraphProps, + PairOfPoints, SegmentGraphState, } from "../types"; import type {vec} from "mafs"; @@ -24,33 +29,129 @@ export function renderSegmentGraph( type SegmentProps = MafsGraphProps; -const SegmentGraph = (props: SegmentProps) => { - const {dispatch} = props; - const {coords: segments} = props.graphState; +const SegmentGraph = ({dispatch, graphState}: SegmentProps) => { + const {coords: segments} = graphState; + const {strings, locale} = usePerseusI18n(); + const segmentUniqueId = React.useId(); + function getWholeSegmentGraphAriaLabel(): string { + return segments?.length > 1 + ? strings.srMultipleSegmentGraphAriaLabel({ + countOfSegments: segments.length, + }) + : strings.srSingleSegmentGraphAriaLabel; + } + + const wholeSegmentGraphAriaLabel = getWholeSegmentGraphAriaLabel(); + // STRING FOR THIS SHOULD BE UPDATED PRIOR TO BEING TRANSLATED + const individualSegmentAriaLabel = strings.srIndividualSegmentAriaLabel; + + function getIndividualSegmentAriaDescription( + segment: PairOfPoints, + index: number, + ) { + return strings.srIndividualSegmentAriaDescription({ + point1X: srFormatNumber(segment[0][X], locale), + point1Y: srFormatNumber(segment[0][Y], locale), + point2X: srFormatNumber(segment[1][X], locale), + point2Y: srFormatNumber(segment[1][Y], locale), + length: srFormatNumber(getLengthOfSegment(segment), locale), + indexOfSegment: index + 1, + }); + } + + function getWholeSegmentGraphAriaDescription() { + let description = `${wholeSegmentGraphAriaLabel} `; + + segments.forEach((segment, index) => { + description += + getIndividualSegmentAriaDescription(segment, index) + " "; + }); + + return description; + } + + function formatSegment( + endpointNumber: number, + x: number, + y: number, + index: number, + ) { + const segObj = { + endpointNumber: endpointNumber, + x: srFormatNumber(x, locale), + y: srFormatNumber(y, locale), + }; + + return segments.length > 1 + ? strings.srMultipleSegmentGraphEndpointAriaLabel({ + ...segObj, + indexOfSegment: index, + }) + : strings.srSingleSegmentGraphEndpointAriaLabel(segObj); + } return ( - <> + {segments?.map((segment, i) => ( - { - dispatch(actions.segment.moveLine(i, delta)); - }} - onMovePoint={( - endpointIndex: number, - destination: vec.Vector2, - ) => { - dispatch( - actions.segment.movePointInFigure( - i, - endpointIndex, - destination, + + { + dispatch(actions.segment.moveLine(i, delta)); + }} + onMovePoint={( + endpointIndex: number, + destination: vec.Vector2, + ) => { + dispatch( + actions.segment.movePointInFigure( + i, + endpointIndex, + destination, + ), + ); + }} + ariaLabels={{ + point1AriaLabel: formatSegment( + 1, + segment[0][X], + segment[0][Y], + i + 1, + ), + point2AriaLabel: formatSegment( + 2, + segment[1][X], + segment[1][Y], + i + 1, ), - ); - }} - /> + }} + /> + + {getIndividualSegmentAriaDescription(segment, i)} + + ))} - + + {getWholeSegmentGraphAriaDescription()} + + ); }; + +function getLengthOfSegment(segment: PairOfPoints) { + return kpoint.distanceToPoint(...segment); +} diff --git a/packages/perseus/src/widgets/interactive-graphs/mafs-graph.test.tsx b/packages/perseus/src/widgets/interactive-graphs/mafs-graph.test.tsx index 6acf462f4b..5020bde723 100644 --- a/packages/perseus/src/widgets/interactive-graphs/mafs-graph.test.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/mafs-graph.test.tsx @@ -154,8 +154,8 @@ describe("MafsGraph", () => { />, ); - expectLabelInDoc("Point 1 at 0 comma 0"); - expectLabelInDoc("Point 2 at -7 comma 0.5"); + expectLabelInDoc("Endpoint 1 at 0 comma 0."); + expectLabelInDoc("Endpoint 2 at -7 comma 0.5."); }); it("renders ARIA labels for each point (multiple segments)", () => { @@ -187,10 +187,10 @@ describe("MafsGraph", () => { />, ); - expectLabelInDoc("Point 1 at 0 comma 0"); - expectLabelInDoc("Point 2 at -7 comma 0.5"); - expectLabelInDoc("Point 1 at 1 comma 1"); - expectLabelInDoc("Point 2 at 7 comma 0.5"); + expectLabelInDoc("Endpoint 1 on segment 1 at 0 comma 0."); + expectLabelInDoc("Endpoint 2 on segment 1 at -7 comma 0.5."); + expectLabelInDoc("Endpoint 1 on segment 2 at 1 comma 1."); + expectLabelInDoc("Endpoint 2 on segment 2 at 7 comma 0.5."); }); it("renders ARIA labels for each point (linear)", () => {