Skip to content

Commit

Permalink
[Locked Figure Aria] Locked point aria label editor UI (#1682)
Browse files Browse the repository at this point in the history
## Summary:
- Create a new `locked-figure-aria.tsx` file that can
  be reused for each locked figure's settings.
- Use this `locked-figure-aria.tsx` component within
  LockedPointSettings.
- Write the function to auto-generate the locked
  point aria label with its coordinates and
  visible labels.

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

## Test plan:
- `yarn jest packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-point-settings.test.tsx`
- `yarn jest packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-figure-aria.test.tsx`

Storybook
- http://localhost:6006/?path=/story/perseuseditor-widgets-interactive-graph--mafs-with-locked-figure-labels-all-flags
- Confirm that the locked point aria label field is already populated with "Point A" (passed in via builder)
- Confirm that pressing "Auto-generate" works with no labels, one label, and multiple labels
- Confirm that updateing the aria label in the editor also updates the aria label on the point
  - Check the web inspector to confirm the aria-label text is updated
  - Use a screen reader to confirm the new label is read out

<img width="399" alt="image" src="https://github.com/user-attachments/assets/3fef8c13-e4f9-4c62-b63b-63bf34824508">

Author: nishasy

Reviewers: catandthemachines, #perseus, benchristel, anakaren-rojas

Required Reviewers:

Approved By: catandthemachines

Checks: ✅ 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), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald

Pull Request URL: #1682
  • Loading branch information
nishasy authored Sep 30, 2024
1 parent 493715e commit 039e0a3
Show file tree
Hide file tree
Showing 6 changed files with 389 additions and 2 deletions.
6 changes: 6 additions & 0 deletions .changeset/strong-spoons-talk.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] Locked point aria label editor UI
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default function InteractiveGraphDescription(props: Props) {
<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.
who may be visually impaired.
</LabelXSmall>
<LabelLarge tag="label">
Title
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {render, screen} from "@testing-library/react";
import {userEvent as userEventLib} from "@testing-library/user-event";
import * as React from "react";

import LockedFigureAria from "./locked-figure-aria";

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

describe("LockedFigureAria", () => {
let userEvent: UserEvent;
beforeEach(() => {
userEvent = userEventLib.setup({
advanceTimers: jest.advanceTimersByTime,
});
});

test("renders", () => {
// Arrange

// Act
render(
<LockedFigureAria
ariaLabel={undefined}
prePopulatedAriaLabel="Pre-populated aria label"
onChangeProps={() => {}}
/>,
);

const titleText = screen.getByText("Aria label");
const descriptionText = screen.getByText(
"The figure is hidden from screen readers if this field is left blank.",
);
const input = screen.getByRole("textbox");
const autoGenButton = screen.getByRole("button", {
name: "Auto-generate",
});

// Assert
expect(titleText).toBeInTheDocument();
expect(descriptionText).toBeInTheDocument();
expect(input).toBeInTheDocument();
expect(input).toHaveValue("");
expect(autoGenButton).toBeInTheDocument();
});

test("renders with aria label", () => {
// Arrange

// Act
render(
<LockedFigureAria
ariaLabel="Point at (x, y)"
prePopulatedAriaLabel="Pre-populated aria label"
onChangeProps={() => {}}
/>,
);

const input = screen.getByRole("textbox");

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

test("auto-generate button calls onChange with the prepopulated label", async () => {
// Arrange
const onChangeProps = jest.fn();

// Act
render(
<LockedFigureAria
ariaLabel={undefined}
prePopulatedAriaLabel="Pre-populated aria label"
onChangeProps={onChangeProps}
/>,
);

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

await userEvent.click(autoGenButton);

// Assert
expect(onChangeProps).toHaveBeenCalledWith({
ariaLabel: "Pre-populated aria label",
});
});

test("calls onChange with undefined when input is cleared", async () => {
// Arrange
const onChangeProps = jest.fn();
render(
<LockedFigureAria
ariaLabel="Point at (x, y)"
prePopulatedAriaLabel="Pre-populated aria label"
onChangeProps={onChangeProps}
/>,
);

// Act
const input = screen.getByRole("textbox");
await userEvent.clear(input);

// Assert
expect(onChangeProps).toHaveBeenCalledWith({
ariaLabel: undefined,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {components} from "@khanacademy/perseus";
import Button from "@khanacademy/wonder-blocks-button";
import {View} from "@khanacademy/wonder-blocks-core";
import {LabeledTextField} from "@khanacademy/wonder-blocks-form";
import {Spring} from "@khanacademy/wonder-blocks-layout";
import {spacing} from "@khanacademy/wonder-blocks-tokens";
import {LabelMedium} from "@khanacademy/wonder-blocks-typography";
import pencilCircle from "@phosphor-icons/core/regular/pencil-circle.svg";
import {StyleSheet} from "aphrodite";
import * as React from "react";

const {InfoTip} = components;

type Props = {
ariaLabel: string | undefined;
prePopulatedAriaLabel: string;
onChangeProps: (props: {ariaLabel?: string | undefined}) => void;
};

function LockedFigureAria(props: Props) {
const {ariaLabel, prePopulatedAriaLabel, onChangeProps} = props;

return (
<View>
<LabeledTextField
label={
<View style={styles.row}>
<LabelMedium>Aria label</LabelMedium>
<Spring />
<InfoTip>
Aria label is used by screen readers to describe
content to users who may be visually impaired.{" "}
<br />
<br />
Populating this field will make it so that users can
use a screen reader to navigate to this point and
hear the description.
<br />
<br />
If you leave this field blank, the point will be
hidden from screen readers. Users will not be able
to navigate to this point using a screen reader.
</InfoTip>
</View>
}
description={`The figure is hidden from screen readers
if this field is left blank.`}
value={ariaLabel ?? ""}
onChange={(newValue) => {
onChangeProps({
// Save as undefined if the field is empty.
ariaLabel: newValue || undefined,
});
}}
placeholder="Ex. Point at (x, y)"
style={styles.ariaLabelTextField}
/>

<Button
kind="tertiary"
startIcon={pencilCircle}
style={styles.button}
onClick={() => {
onChangeProps({
ariaLabel: prePopulatedAriaLabel,
});
}}
>
Auto-generate
</Button>
</View>
);
}

const styles = StyleSheet.create({
row: {
flexDirection: "row",
alignItems: "center",
},
button: {
alignSelf: "start",
},
ariaLabelTextField: {
marginTop: spacing.xSmall_8,
},
});

export default LockedFigureAria;
Original file line number Diff line number Diff line change
Expand Up @@ -347,4 +347,134 @@ describe("LockedPointSettings", () => {
],
});
});

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

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

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

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

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

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

// Assert
expect(onChangeProps).toHaveBeenCalledWith({
ariaLabel: "Point at (0, 0)",
});
});

test("aria label auto-generates (one label)", async () => {
// Arrange
const onChangeProps = jest.fn();
render(
<LockedPointSettings
{...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: "Point at (0, 0) with label A",
});
});

test("aria label auto-generates (multiple labels)", async () => {
// Arrange
const onChangeProps = jest.fn();
render(
<LockedPointSettings
{...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: "Point at (0, 0) with labels A, B",
});
});
});
Loading

0 comments on commit 039e0a3

Please sign in to comment.