Skip to content

Commit

Permalink
[Interactive Graph + Editor] Add aria label and description to entire…
Browse files Browse the repository at this point in the history
… interactive graph and its UI to the editor (#1568)

## Summary:
- Add `fullGraphAriaLabel` and `fullGraphAriaDescription` fields to interactive graph.
- Hook up the graph so that if the `fullGraphAriaLabel` and/or `fullGraphAriaDescription` fields are present, then the graph has the respective `aria-label` and `aria-describedby` properties.
- Add the UI to the interactive graph editor that allows the content author to add a title (label) and description to the graph.

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

## Test plan:
`yarn jest packages/perseus-editor/src/components/__tests__/interactive-graph-description.test.tsx`
`yarn jest packages/perseus-editor/src/widgets/__tests__/interactive-graph-editor.test.tsx`
`yarn jest packages/perseus/src/widgets/interactive-graphs/interactive-graph.test.tsx`

Storybook
- Go to http://localhost:6006/iframe.html?id=perseuseditor-widgets-interactive-graph--interactive-graph-with-aria-label&viewMode=story
- There should already be a title and description written in their respective fields
- Use a screenreader to tab to the graph
- Confirm that the title and description are read. They will both be read at once in Safari and Firefox. With Chrome, use the screenreader controls to go to the next element to hear the description.
- Change the values in the title and description inputs, and confirm that the aria label and description on the graph updates.

## Demo:

https://github.com/user-attachments/assets/3c825e87-a989-4327-8319-2205e8913b4e

NOTE! The janky textarea will be replaced with a Wonder Blocks TextArea [soon](https://khanacademy.atlassian.net/browse/LEMS-2332).

![image](https://github.com/user-attachments/assets/a4b6c931-4619-4c71-bb35-542cea6817fd)

Author: nishasy

Reviewers: benchristel, mark-fitzgerald, catandthemachines, jeremywiebe

Required Reviewers:

Approved By: benchristel

Checks: ✅ codecov/project, ✅ gerald, ✅ Upload Coverage (ubuntu-latest, 20.x), ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Jest Coverage (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), 🚫 Upload Coverage, ✅ gerald, 🚫 Publish npm snapshot (ubuntu-latest, 20.x), 🚫 Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), 🚫 Jest Coverage (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), 🚫 Check builds for changes in size (ubuntu-latest, 20.x), 🚫 Cypress (ubuntu-latest, 20.x), ✅ gerald, ✅ codecov/patch, ✅ Upload Coverage (ubuntu-latest, 20.x), ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Jest Coverage (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ gerald

Pull Request URL: #1568
  • Loading branch information
nishasy authored Sep 6, 2024
1 parent 6c4e9e1 commit eddcb94
Show file tree
Hide file tree
Showing 12 changed files with 346 additions and 1 deletion.
6 changes: 6 additions & 0 deletions .changeset/new-pigs-begin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@khanacademy/perseus": minor
"@khanacademy/perseus-editor": minor
---

[Interactive Graph + Editor] Add a full graph aria-label and aria-description/describedby to interactive graphs, as well as the UI for content authors to add this in the interactive graph editor
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {EditorPage} from "..";
import {
angleWithStartingCoordsQuestion,
circleWithStartingCoordsQuestion,
interactiveGraphWithAriaLabel,
linearSystemWithStartingCoordsQuestion,
linearWithStartingCoordsQuestion,
pointQuestionWithStartingCoords,
Expand Down Expand Up @@ -41,6 +42,10 @@ export default {

const onChangeAction = action("onChange");

export const InteractiveGraphWithAriaLabel = (): React.ReactElement => (
<EditorPageWithStorybookPreview question={interactiveGraphWithAriaLabel} />
);

export const InteractiveGraphSegment = (): React.ReactElement => {
return (
<EditorPageWithStorybookPreview
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import {Dependencies} from "@khanacademy/perseus";
import {render, screen} from "@testing-library/react";
import {userEvent as userEventLib} from "@testing-library/user-event";
import * as React from "react";

import {testDependencies} from "../../../../../testing/test-dependencies";
import InteractiveGraphDescription from "../interactive-graph-description";

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

import "@testing-library/jest-dom"; // Imports custom matchers

function userEventForFakeTimers() {
return userEventLib.setup({
advanceTimers: jest.advanceTimersByTime,
});
}

describe("InteractiveGraphSettings", () => {
let userEvent: UserEvent;
beforeEach(() => {
userEvent = userEventForFakeTimers();
jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
testDependencies,
);
});

test("renders", () => {
// Arrange
render(
<InteractiveGraphDescription
ariaLabelValue="Graph Title"
ariaDescriptionValue="Graph Description"
onChange={jest.fn()}
/>,
);

// Act
const titleInput = screen.getByRole("textbox", {name: "Title"});
const descriptionInput = screen.getByRole("textbox", {
name: "Description",
});

// Assert
expect(titleInput).toBeInTheDocument();
expect(titleInput).toHaveValue("Graph Title");
expect(descriptionInput).toBeInTheDocument();
expect(descriptionInput).toHaveValue("Graph Description");
});

test("calls onChange when the title is changed", async () => {
// Arrange
const onChange = jest.fn();
render(
<InteractiveGraphDescription
ariaLabelValue=""
ariaDescriptionValue=""
onChange={onChange}
/>,
);

// Act
const titleInput = screen.getByRole("textbox", {name: "Title"});
await userEvent.clear(titleInput);
await userEvent.type(titleInput, "Zot");

// Assert
// Calls are not being accumulated because they're mocked.
expect(onChange.mock.calls).toEqual([
[{fullGraphAriaLabel: "Z"}],
[{fullGraphAriaLabel: "o"}],
[{fullGraphAriaLabel: "t"}],
]);
});

test("calls onChange when the description is changed", async () => {
// Arrange
const onChange = jest.fn();
render(
<InteractiveGraphDescription
ariaLabelValue=""
ariaDescriptionValue=""
onChange={onChange}
/>,
);

// Act
const descriptionInput = screen.getByRole("textbox", {
name: "Description",
});
await userEvent.clear(descriptionInput);
await userEvent.type(descriptionInput, "Zot");

// Assert
// Calls are not being accumulated because they're mocked.
expect(onChange.mock.calls).toEqual([
[{fullGraphAriaDescription: "Z"}],
[{fullGraphAriaDescription: "o"}],
[{fullGraphAriaDescription: "t"}],
]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {View} from "@khanacademy/wonder-blocks-core";
import {TextField} from "@khanacademy/wonder-blocks-form";
import {Strut} from "@khanacademy/wonder-blocks-layout";
import {color, spacing} from "@khanacademy/wonder-blocks-tokens";
import {LabelLarge, LabelXSmall} from "@khanacademy/wonder-blocks-typography";
import {StyleSheet} from "aphrodite";
import * as React from "react";

import Heading from "./heading";

import type {Props as EditorProps} from "../widgets/interactive-graph-editor/interactive-graph-editor";

type Props = {
ariaLabelValue: string;
ariaDescriptionValue: string;
onChange: (graphProps: Partial<EditorProps>) => void;
};

export default function InteractiveGraphDescription(props: Props) {
const {ariaLabelValue, ariaDescriptionValue, onChange} = props;

const [isOpen, setIsOpen] = React.useState(true);

return (
<>
<Heading
title="Description"
isCollapsible={true}
isOpen={isOpen}
onToggle={setIsOpen}
/>
{isOpen && (
<View>
<LabelXSmall style={styles.caption}>
Use these fields to describe the graph as a whole. These
are used by screen readers to describe content to users
who are visually impaired.
</LabelXSmall>
<LabelLarge tag="label">
Title
<TextField
value={ariaLabelValue}
onChange={(newValue) =>
onChange({fullGraphAriaLabel: newValue})
}
/>
</LabelLarge>
<Strut size={spacing.xSmall_8} />
<LabelLarge
tag="label"
// TODO(LEMS-2332): Remove this style prop after
// switching to WB TextArea
style={{
display: "flex",
flexDirection: "column",
}}
>
Description
{/* TODO(LEMS-2332): Change this to a WB TextArea */}
<textarea
rows={8}
value={ariaDescriptionValue}
onChange={(e) =>
onChange({
fullGraphAriaDescription: e.target.value,
})
}
/>
</LabelLarge>
</View>
)}
</>
);
}

const styles = StyleSheet.create({
caption: {
color: color.offBlack64,
paddingTop: spacing.xxSmall_6,
paddingBottom: spacing.xxSmall_6,
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -1066,4 +1066,28 @@ describe("InteractiveGraphEditor", () => {
}),
).toBeNull();
});

test("shows description section", () => {
// Arrange

// Act
render(
<InteractiveGraphEditor
{...mafsProps}
graph={{type: "segment"}}
correct={{type: "segment"}}
/>,
{
wrapper: RenderStateRoot,
},
);

// Assert
expect(
screen.getByRole("textbox", {name: "Title"}),
).toBeInTheDocument();
expect(
screen.getByRole("textbox", {name: "Description"}),
).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import LockedFiguresSection from "../../components/graph-locked-figures/locked-f
import GraphPointsCountSelector from "../../components/graph-points-count-selector";
import GraphTypeSelector from "../../components/graph-type-selector";
import {InteractiveGraphCorrectAnswer} from "../../components/interactive-graph-correct-answer";
import InteractiveGraphDescription from "../../components/interactive-graph-description";
import InteractiveGraphSettings from "../../components/interactive-graph-settings";
import SegmentCountSelector from "../../components/segment-count-selector";
import StartCoordsSettings from "../../components/start-coords-settings";
Expand Down Expand Up @@ -126,6 +127,13 @@ export type Props = {
* etc.) that are locked in place and not interactive.
*/
lockedFigures?: Array<LockedFigure>;
// Aria-label for the full graph area. Short title for the graph.
fullGraphAriaLabel?: string;
// Aria-description for the graph area. Longer description of the graph.
// Note that the `aria-description` property is not supported well,
// so this description will be hidden in a DOM element whose ID will
// then be referenced by the graph's `aria-describedby` property.
fullGraphAriaDescription?: string;

/**
* The graph to display in the graph area.
Expand Down Expand Up @@ -205,6 +213,8 @@ class InteractiveGraphEditor extends React.Component<Props> {
"gridStep",
"snapStep",
"lockedFigures",
"fullGraphAriaLabel",
"fullGraphAriaDescription",
);

// eslint-disable-next-line react/no-string-refs
Expand Down Expand Up @@ -293,6 +303,8 @@ class InteractiveGraphEditor extends React.Component<Props> {
showProtractor: this.props.showProtractor,
showTooltips: this.props.showTooltips,
lockedFigures: this.props.lockedFigures,
fullGraphAriaLabel: this.props.fullGraphAriaLabel,
fullGraphAriaDescription: this.props.fullGraphAriaDescription,
trackInteraction: function () {},
onChange: (newProps: InteractiveGraphProps) => {
let correct = this.props.correct;
Expand Down Expand Up @@ -353,6 +365,18 @@ class InteractiveGraphEditor extends React.Component<Props> {
}}
/>
</LabeledRow>
{this.props.graph &&
this.props.apiOptions?.flags?.mafs?.[
this.props.graph.type
] && (
<InteractiveGraphDescription
ariaLabelValue={this.props.fullGraphAriaLabel ?? ""}
ariaDescriptionValue={
this.props.fullGraphAriaDescription ?? ""
}
onChange={this.props.onChange}
/>
)}
<InteractiveGraphCorrectAnswer equationString={equationString}>
{graph}
</InteractiveGraphCorrectAnswer>
Expand Down
4 changes: 4 additions & 0 deletions packages/perseus/src/perseus-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,10 @@ export type PerseusInteractiveGraphWidgetOptions = {
// Shapes (points, chords, etc) displayed on the graph that cannot
// be moved by the user.
lockedFigures?: ReadonlyArray<LockedFigure>;
// Aria label that applies to the entire graph.
fullGraphAriaLabel?: string;
// Aria description that applies to the entire graph.
fullGraphAriaDescription?: string;
};

const lockedFigureColorNames = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export function interactiveGraphQuestionBuilder(): InteractiveGraphQuestionBuild

class InteractiveGraphQuestionBuilder {
private content: string = "[[☃ interactive-graph 1]]";
private fullGraphAriaLabel?: string;
private fullGraphAriaDescription?: string;
private backgroundImage?: {
url: string;
height: number;
Expand Down Expand Up @@ -61,6 +63,8 @@ class InteractiveGraphQuestionBuilder {
static: this.staticMode,
options: {
correct: this.interactiveFigureConfig.correct(),
fullGraphAriaLabel: this.fullGraphAriaLabel,
fullGraphAriaDescription: this.fullGraphAriaDescription,
backgroundImage: this.backgroundImage,
graph: this.interactiveFigureConfig.graph(),
gridStep: this.gridStep,
Expand All @@ -87,6 +91,18 @@ class InteractiveGraphQuestionBuilder {
return this;
}

withFullGraphAriaLabel(label: string): InteractiveGraphQuestionBuilder {
this.fullGraphAriaLabel = label;
return this;
}

withFullGraphAriaDescription(
description: string,
): InteractiveGraphQuestionBuilder {
this.fullGraphAriaDescription = description;
return this;
}

withStaticMode(staticMode: boolean): InteractiveGraphQuestionBuilder {
this.staticMode = staticMode;
return this;
Expand Down
Loading

0 comments on commit eddcb94

Please sign in to comment.