Skip to content

Commit

Permalink
[Locked Figure Aria] Implement locked vector aria labels (graph + edi…
Browse files Browse the repository at this point in the history
…tor)
  • Loading branch information
nishasy committed Sep 26, 2024
1 parent 6340759 commit 1003256
Show file tree
Hide file tree
Showing 10 changed files with 254 additions and 7 deletions.
6 changes: 6 additions & 0 deletions .changeset/red-garlics-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@khanacademy/perseus": minor
"@khanacademy/perseus-editor": minor
---

[Locked Figure Aria] Implement locked vector aria labels (graph + editor)
Original file line number Diff line number Diff line change
Expand Up @@ -366,4 +366,136 @@ describe("Locked Vector Settings", () => {
});
});
});

describe("Aria label", () => {
test("Renders with aria label", () => {
// Arrange

// Act
render(
<LockedVectorSettings
{...defaultProps}
ariaLabel="Vector at (x, y)"
/>,
{wrapper: RenderStateRoot},
);

const input = screen.getByRole("textbox", {name: "Aria label"});

// Assert
expect(input).toHaveValue("Vector at (x, y)");
});

test("calls onChangeProps when the aria label is updated", async () => {
// Arrange
const onChangeProps = jest.fn();
render(
<LockedVectorSettings
{...defaultProps}
ariaLabel={undefined}
onChangeProps={onChangeProps}
/>,
{wrapper: RenderStateRoot},
);

// Act
const input = screen.getByRole("textbox", {name: "Aria label"});
await userEvent.clear(input);
await userEvent.type(input, "A");

// Assert
expect(onChangeProps).toHaveBeenCalledWith({
ariaLabel: "A",
});
});

test("aria label auto-generates (no labels)", async () => {
// Arrange
const onChangeProps = jest.fn();

// Act
render(
<LockedVectorSettings
{...defaultProps}
ariaLabel={undefined}
onChangeProps={onChangeProps}
/>,
{wrapper: RenderStateRoot},
);

const autoGenButton = screen.getByRole("button", {
name: "Auto-generate",
});
await userEvent.click(autoGenButton);

// Assert
expect(onChangeProps).toHaveBeenCalledWith({
ariaLabel: "Vector from (0, 0) to (2, 2)",
});
});

test("aria label auto-generates (one label)", async () => {
// Arrange
const onChangeProps = jest.fn();
render(
<LockedVectorSettings
{...defaultProps}
ariaLabel={undefined}
onChangeProps={onChangeProps}
labels={[
{
...defaultLabel,
text: "A",
},
]}
/>,
{wrapper: RenderStateRoot},
);

// Act
const autoGenButton = screen.getByRole("button", {
name: "Auto-generate",
});
await userEvent.click(autoGenButton);

// Assert
expect(onChangeProps).toHaveBeenCalledWith({
ariaLabel: "Vector from (0, 0) to (2, 2) with label A",
});
});

test("aria label auto-generates (multiple labels)", async () => {
// Arrange
const onChangeProps = jest.fn();
render(
<LockedVectorSettings
{...defaultProps}
ariaLabel={undefined}
onChangeProps={onChangeProps}
labels={[
{
...defaultLabel,
text: "A",
},
{
...defaultLabel,
text: "B",
},
]}
/>,
{wrapper: RenderStateRoot},
);

// Act
const autoGenButton = screen.getByRole("button", {
name: "Auto-generate",
});
await userEvent.click(autoGenButton);

// Assert
expect(onChangeProps).toHaveBeenCalledWith({
ariaLabel: "Vector from (0, 0) to (2, 2) with labels A, B",
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import PerseusEditorAccordion from "../../../components/perseus-editor-accordion

import ColorSelect from "./color-select";
import LineSwatch from "./line-swatch";
import LockedFigureAria from "./locked-figure-aria";
import LockedFigureSettingsActions from "./locked-figure-settings-actions";
import LockedLabelSettings from "./locked-label-settings";
import {getDefaultFigureForType} from "./util";
Expand Down Expand Up @@ -49,6 +50,7 @@ const LockedVectorSettings = (props: Props) => {
points,
color: lineColor,
labels,
ariaLabel,
onChangeProps,
onMove,
onRemove,
Expand All @@ -59,6 +61,29 @@ const LockedVectorSettings = (props: Props) => {
// Check if the line has length 0.
const isInvalid = kvector.equal(tail, tip);

function getPrepopulatedAriaLabel() {
let str = `Vector from (${tail[0]}, ${tail[1]}) to (${tip[0]}, ${tip[1]})`;

if (labels && labels.length > 0) {
str += " with label";
// Make it "with labels" instead of "with label" if there are
// multiple labels.
if (labels.length > 1) {
str += "s";
}

for (let i = 0; i < labels.length; i++) {
// Separate additional labels with commas.
if (i > 0) {
str += ",";
}
str += ` ${labels[i].text}`;
}
}

return str;
}

function handleChangePoint(newCoord: Coord | undefined, index: 0 | 1) {
if (typeof newCoord !== "undefined") {
const newPoints = [...points] satisfies [tail: Coord, tip: Coord];
Expand Down Expand Up @@ -187,9 +212,28 @@ const LockedVectorSettings = (props: Props) => {
/>
</PerseusEditorAccordion>

{flags?.["mafs"]?.["locked-figures-aria"] && (
<>
<Strut size={spacing.small_12} />
<View style={styles.horizontalRule} />

<LockedFigureAria
ariaLabel={ariaLabel}
prePopulatedAriaLabel={getPrepopulatedAriaLabel()}
onChangeProps={(newProps) => {
onChangeProps(newProps);
}}
/>
</>
)}

{flags?.["mafs"]?.["locked-vector-labels"] && (
<>
<Strut size={spacing.xxxSmall_4} />
<View style={styles.horizontalRule} />
<Strut size={spacing.small_12} />

<LabelMedium>Visible labels</LabelMedium>

{labels?.map((label, labelIndex) => (
<LockedLabelSettings
Expand Down Expand Up @@ -268,8 +312,6 @@ const styles = StyleSheet.create({
alignSelf: "start",
},
horizontalRule: {
marginTop: spacing.small_12,
marginBottom: spacing.xxxSmall_4,
height: 1,
backgroundColor: wbColor.offBlack16,
},
Expand Down
1 change: 1 addition & 0 deletions packages/perseus/src/perseus-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,7 @@ export type LockedVectorType = {
points: [tail: Coord, tip: Coord];
color: LockedFigureColor;
labels?: LockedLabelType[];
ariaLabel?: string;
};

export type LockedFigureFillType = "none" | "white" | "translucent" | "solid";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ const GraphLockedLayer = (props: Props) => {
);
case "vector":
return (
<LockedVector key={`vector-${index}`} {...figure} />
<LockedVector
key={`vector-${index}`}
{...figure}
flags={flags}
/>
);
case "ellipse":
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,7 @@ describe("InteractiveGraphQuestionBuilder", () => {
.addLockedVector([1, 2], [3, 4], {
color: "green",
labels: [{text: "a label"}],
ariaLabel: "an aria label",
})
.build();
const graph = question.widgets["interactive-graph 1"];
Expand All @@ -1001,6 +1002,7 @@ describe("InteractiveGraphQuestionBuilder", () => {
size: "medium",
},
],
ariaLabel: "an aria label",
},
]);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ class InteractiveGraphQuestionBuilder {
options?: {
color?: LockedFigureColor;
labels?: LockedFigureLabelOptions[];
ariaLabel?: string;
},
): InteractiveGraphQuestionBuilder {
const vector: LockedVectorType = {
Expand All @@ -372,6 +373,7 @@ class InteractiveGraphQuestionBuilder {
color: options?.color ?? "grayH",
size: label.size ?? "medium",
})),
ariaLabel: options?.ariaLabel,
};
this.addLockedFigure(vector);
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,52 @@ describe("locked layer", () => {
);
});

it("should render locked vector with aria label when one is provided", () => {
// Arrange
const lockedVectorWithAriaLabelQuestion =
interactiveGraphQuestionBuilder()
.addLockedVector([0, 0], [2, 2], {
ariaLabel: "Vector A",
})
.build();
const {container} = renderQuestion(lockedVectorWithAriaLabelQuestion, {
flags: {
mafs: {
segment: true,
"locked-figures-aria": true,
},
},
});

// Act
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
const point = container.querySelector(".locked-vector");

// Assert
expect(point).toHaveAttribute("aria-label", "Vector A");
});

it("should render locked vector without aria label by default", () => {
// Arrange
const simpleLockedVectorquestion = interactiveGraphQuestionBuilder()
.addLockedVector([0, 0], [2, 2])
.build();
const {container} = renderQuestion(simpleLockedVectorquestion, {
flags: {
mafs: {
segment: true,
},
},
});

// Act
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
const point = container.querySelector(".locked-vector");

// Assert
expect(point).not.toHaveAttribute("aria-label");
});

it("should render locked ellipses", async () => {
// Arrange
const {container} = renderQuestion(segmentWithLockedEllipses, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -835,11 +835,12 @@ export const segmentWithLockedFigures: PerseusRenderer =
showPoint1: true,
showPoint2: true,
labels: [{text: "B"}],
ariaLabel: "Line PQ",
ariaLabel: "Line B",
})
.addLockedVector([0, 0], [8, 2], {
color: "purple",
labels: [{text: "C"}],
ariaLabel: "Vector C",
})
.addLockedEllipse([0, 5], [4, 2], {
angle: Math.PI / 4,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,24 @@ import {lockedFigureColors} from "../../../perseus-types";
import {Vector} from "../graphs/components/vector";

import type {LockedVectorType} from "../../../perseus-types";
import type {APIOptions} from "../../../types";

const LockedVector = (props: LockedVectorType) => {
const {color, points} = props;
type Props = LockedVectorType & {
flags?: APIOptions["flags"];
};

const LockedVector = (props: Props) => {
const {color, points, ariaLabel, flags} = props;
const [tail, tip] = points;

const hasAria = ariaLabel && flags?.["mafs"]?.["locked-figures-aria"];

return (
<g className="locked-vector">
<g
className="locked-vector"
aria-label={hasAria ? ariaLabel : undefined}
aria-hidden={!hasAria}
>
<Vector tail={tail} tip={tip} color={lockedFigureColors[color]} />
</g>
);
Expand Down

0 comments on commit 1003256

Please sign in to comment.