Skip to content

Commit

Permalink
[Locked Figure Aria] Use spoken math in locked figure settings autoge…
Browse files Browse the repository at this point in the history
…n labels (#1858)

## Summary:
When auto-generating the aria labels for locked figures, we want it to use
words as if they were spoken rather than math expressions that might be
read incorrectly by the screen reader.

- Use the `generateSpokenMathDetails` utility within LockedVectorSettings,
  LockedEllipseSettings, LockedPolygonSettings, and LockedFunctionSettings.
- Updated LockedFigureAria - remove the prop `prePopulatedAriaLabel` and
  make `getPrepopulatedAriaLabel` required.

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

## Test plan:
- `yarn jest packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-vector-settings.test.tsx`
- `yarn jest packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-ellipse-settings.test.tsx`
- `yarn jest packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-polygon-settings.test.tsx`
- `yarn jest packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-function-settings.test.tsx`

Storybook
- Go to http://localhost:6006/iframe.html?args=&id=perseuseditor-widgets-interactive-graph--mafs-with-locked-figure-labels-all-flags&viewMode=story
- For each locked figure:
  - Open the figure settings
  - Change the visible label to have a mix of TeX and non-TeX
  - Press the "Auto-generate" button
  - Confirm that the input changes to include spoken math words for the TeX
  - Also try this with no labels, multiple labels, fully non-TeX, and
    fully teX labels.

Author: nishasy

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

Required Reviewers:

Approved By: anakaren-rojas, catandthemachines

Checks: ✅ Publish npm snapshot (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), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald

Pull Request URL: #1858
  • Loading branch information
nishasy authored Nov 15, 2024
1 parent ef0ad98 commit 5e930ce
Show file tree
Hide file tree
Showing 12 changed files with 127 additions and 55 deletions.
6 changes: 6 additions & 0 deletions .changeset/rare-lamps-cough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@khanacademy/perseus": patch
"@khanacademy/perseus-editor": patch
---

[Locked Figure Labels] Util function to generate spoken math + use it within Locked Point aria labels
5 changes: 5 additions & 0 deletions .changeset/short-actors-sit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus-editor": patch
---

[Locked Figure Aria] Use spoken math in locked figure settings autogen labels
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ const defaultProps = {

const defaultLabel = getDefaultFigureForType("label");

// Mock the async function generateSpokenMathDetails
jest.mock("./util", () => ({
...jest.requireActual("./util"),
generateSpokenMathDetails: (input) => {
return Promise.resolve(`Spoken math details for ${input}`);
},
}));

describe("LockedEllipseSettings", () => {
let userEvent: UserEvent;
beforeEach(() => {
Expand Down Expand Up @@ -383,7 +391,7 @@ describe("LockedEllipseSettings", () => {
// Assert
expect(onChangeProps).toHaveBeenCalledWith({
ariaLabel:
"Circle with radius 2, centered at (0, 0). Appearance solid gray border, with no fill.",
"Spoken math details for Circle with radius 2, centered at (0, 0). Appearance solid gray border, with no fill.",
});
});

Expand Down Expand Up @@ -411,7 +419,7 @@ describe("LockedEllipseSettings", () => {
// Assert
expect(onChangeProps).toHaveBeenCalledWith({
ariaLabel:
"Circle with radius 2, centered at (0, 0). Appearance solid gray border, with no fill.",
"Spoken math details for Circle with radius 2, centered at (0, 0). Appearance solid gray border, with no fill.",
});
});

Expand All @@ -438,7 +446,7 @@ describe("LockedEllipseSettings", () => {
// Assert
expect(onChangeProps).toHaveBeenCalledWith({
ariaLabel:
"Ellipse with x radius 2 and y radius 3, centered at (0, 0). Appearance solid gray border, with no fill.",
"Spoken math details for Ellipse with x radius 2 and y radius 3, centered at (0, 0). Appearance solid gray border, with no fill.",
});
});

Expand Down Expand Up @@ -466,7 +474,7 @@ describe("LockedEllipseSettings", () => {
// Assert
expect(onChangeProps).toHaveBeenCalledWith({
ariaLabel:
"Ellipse with x radius 2 and y radius 3, centered at (0, 0), rotated by 90 degrees. Appearance solid gray border, with no fill.",
"Spoken math details for Ellipse with x radius 2 and y radius 3, centered at (0, 0), rotated by 90 degrees. Appearance solid gray border, with no fill.",
});
});

Expand Down Expand Up @@ -498,7 +506,7 @@ describe("LockedEllipseSettings", () => {
// Assert
expect(onChangeProps).toHaveBeenCalledWith({
ariaLabel:
"Circle A with radius 2, centered at (0, 0). Appearance solid gray border, with no fill.",
"Spoken math details for Circle A with radius 2, centered at (0, 0). Appearance solid gray border, with no fill.",
});
});

Expand Down Expand Up @@ -534,7 +542,7 @@ describe("LockedEllipseSettings", () => {
// Assert
expect(onChangeProps).toHaveBeenCalledWith({
ariaLabel:
"Circle A, B with radius 2, centered at (0, 0). Appearance solid gray border, with no fill.",
"Spoken math details for Circle A, B with radius 2, centered at (0, 0). Appearance solid gray border, with no fill.",
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import LockedFigureSettingsActions from "./locked-figure-settings-actions";
import LockedLabelSettings from "./locked-label-settings";
import {
generateLockedFigureAppearanceDescription,
generateSpokenMathDetails,
getDefaultFigureForType,
} from "./util";

Expand Down Expand Up @@ -62,7 +63,11 @@ const LockedEllipseSettings = (props: Props) => {
onRemove,
} = props;

function getPrepopulatedAriaLabel() {
/**
* Generate the prepopulated aria label for the ellipse,
* with the math details converted into spoken words.
*/
async function getPrepopulatedAriaLabel() {
let visiblelabel = "";
if (labels && labels.length > 0) {
visiblelabel += ` ${labels.map((l) => l.text).join(", ")}`;
Expand All @@ -72,9 +77,13 @@ const LockedEllipseSettings = (props: Props) => {
let str = "";

if (isCircle) {
str += `Circle${visiblelabel} with radius ${radius[0]}`;
str += await generateSpokenMathDetails(
`Circle${visiblelabel} with radius ${radius[0]}`,
);
} else {
str += `Ellipse${visiblelabel} with x radius ${radius[0]} and y radius ${radius[1]}`;
str += await generateSpokenMathDetails(
`Ellipse${visiblelabel} with x radius ${radius[0]} and y radius ${radius[1]}`,
);
}

str += `, centered at (${center[0]}, ${center[1]})`;
Expand Down Expand Up @@ -250,7 +259,7 @@ const LockedEllipseSettings = (props: Props) => {

<LockedFigureAria
ariaLabel={ariaLabel}
prePopulatedAriaLabel={getPrepopulatedAriaLabel()}
getPrepopulatedAriaLabel={getPrepopulatedAriaLabel}
onChangeProps={(newProps) => {
onChangeProps(newProps);
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ describe("LockedFigureAria", () => {
render(
<LockedFigureAria
ariaLabel={undefined}
prePopulatedAriaLabel="Pre-populated aria label"
getPrepopulatedAriaLabel={() =>
Promise.resolve("Pre-populated aria label")
}
onChangeProps={() => {}}
/>,
{wrapper: RenderStateRoot},
Expand Down Expand Up @@ -52,7 +54,9 @@ describe("LockedFigureAria", () => {
render(
<LockedFigureAria
ariaLabel="Point at (x, y)"
prePopulatedAriaLabel="Pre-populated aria label"
getPrepopulatedAriaLabel={() =>
Promise.resolve("Pre-populated aria label")
}
onChangeProps={() => {}}
/>,
{wrapper: RenderStateRoot},
Expand All @@ -72,7 +76,9 @@ describe("LockedFigureAria", () => {
render(
<LockedFigureAria
ariaLabel={undefined}
prePopulatedAriaLabel="Pre-populated aria label"
getPrepopulatedAriaLabel={() =>
Promise.resolve("Pre-populated aria label")
}
onChangeProps={onChangeProps}
/>,
{wrapper: RenderStateRoot},
Expand All @@ -96,7 +102,9 @@ describe("LockedFigureAria", () => {
render(
<LockedFigureAria
ariaLabel="Point at (x, y)"
prePopulatedAriaLabel="Pre-populated aria label"
getPrepopulatedAriaLabel={() =>
Promise.resolve("Pre-populated aria label")
}
onChangeProps={onChangeProps}
/>,
{wrapper: RenderStateRoot},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,16 @@ const {InfoTip} = components;

type Props = {
ariaLabel: string | undefined;
prePopulatedAriaLabel?: string;
getPrepopulatedAriaLabel?: () => Promise<string>;
/**
* The async function that generates the prepopulated aria label
* for the locked figure with math details converted to spoken words.
*/
getPrepopulatedAriaLabel: () => Promise<string>;
onChangeProps: (props: {ariaLabel?: string | undefined}) => void;
};

function LockedFigureAria(props: Props) {
const {
ariaLabel,
prePopulatedAriaLabel,
getPrepopulatedAriaLabel,
onChangeProps,
} = props;
const {ariaLabel, getPrepopulatedAriaLabel, onChangeProps} = props;
const id = React.useId();
const ariaLabelId = `aria-label-${id}`;

Expand Down Expand Up @@ -77,18 +75,11 @@ function LockedFigureAria(props: Props) {
startIcon={pencilCircle}
style={styles.button}
onClick={() => {
// TODO(LEMS-2548): remove the prePopulatedAriaLabel prop
// after all the locked figures are updated to use
// getPrepopulatedAriaLabel.
if (prePopulatedAriaLabel) {
onChangeProps({ariaLabel: prePopulatedAriaLabel});
} else if (getPrepopulatedAriaLabel) {
setLoading(true);
getPrepopulatedAriaLabel().then((ariaLabel) => {
setLoading(false);
onChangeProps({ariaLabel});
});
}
setLoading(true);
getPrepopulatedAriaLabel().then((ariaLabel) => {
setLoading(false);
onChangeProps({ariaLabel});
});
}}
>
Auto-generate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ const defaultProps = {

const defaultLabel = getDefaultFigureForType("label");

// Mock the async function generateSpokenMathDetails
jest.mock("./util", () => ({
...jest.requireActual("./util"),
generateSpokenMathDetails: (input) => {
return Promise.resolve(`Spoken math details for ${input}`);
},
}));

const exampleEquationsMock = {
foo: ["bar", "zot"],
};
Expand Down Expand Up @@ -679,7 +687,7 @@ describe("Locked Function Settings", () => {
// Assert
expect(onChangeProps).toHaveBeenCalledWith({
ariaLabel:
"Function with equation y=x^2. Appearance solid gray.",
"Spoken math details for Function with equation y=x^2. Appearance solid gray.",
});
});

Expand All @@ -706,7 +714,7 @@ describe("Locked Function Settings", () => {
// Assert
expect(onChangeProps).toHaveBeenCalledWith({
ariaLabel:
"Function with equation x=y^2. Appearance solid gray.",
"Spoken math details for Function with equation x=y^2. Appearance solid gray.",
});
});

Expand All @@ -732,7 +740,7 @@ describe("Locked Function Settings", () => {
// Assert
expect(onChangeProps).toHaveBeenCalledWith({
ariaLabel:
"Function with equation y=x^2, domain from 1 to 2. Appearance solid gray.",
"Spoken math details for Function with equation y=x^2, domain from 1 to 2. Appearance solid gray.",
});
});

Expand All @@ -758,7 +766,7 @@ describe("Locked Function Settings", () => {
// Assert
expect(onChangeProps).toHaveBeenCalledWith({
ariaLabel:
"Function with equation y=x^2. Appearance solid gray.",
"Spoken math details for Function with equation y=x^2. Appearance solid gray.",
});
});

Expand Down Expand Up @@ -789,7 +797,7 @@ describe("Locked Function Settings", () => {
// Assert
expect(onChangeProps).toHaveBeenCalledWith({
ariaLabel:
"Function A with equation y=x^2. Appearance solid gray.",
"Spoken math details for Function A with equation y=x^2. Appearance solid gray.",
});
});

Expand Down Expand Up @@ -824,7 +832,7 @@ describe("Locked Function Settings", () => {
// Assert
expect(onChangeProps).toHaveBeenCalledWith({
ariaLabel:
"Function A, B with equation y=x^2. Appearance solid gray.",
"Spoken math details for Function A, B with equation y=x^2. Appearance solid gray.",
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import examples from "./locked-function-examples";
import LockedLabelSettings from "./locked-label-settings";
import {
generateLockedFigureAppearanceDescription,
generateSpokenMathDetails,
getDefaultFigureForType,
} from "./util";

Expand Down Expand Up @@ -84,13 +85,19 @@ const LockedFunctionSettings = (props: Props) => {
]);
}, [domain]);

function getPrepopulatedAriaLabel() {
/**
* Generate the prepopulated aria label for the polygon,
* with the math details converted into spoken words.
*/
async function getPrepopulatedAriaLabel() {
let visiblelabel = "";
if (labels && labels.length > 0) {
visiblelabel += ` ${labels.map((l) => l.text).join(", ")}`;
}

let str = `Function${visiblelabel} with equation ${equationPrefix}${equation}`;
let str = await generateSpokenMathDetails(
`Function${visiblelabel} with equation ${equationPrefix}${equation}`,
);

// Add the domain/range constraints to the aria label
// if they are not the default values.
Expand Down Expand Up @@ -331,7 +338,7 @@ const LockedFunctionSettings = (props: Props) => {

<LockedFigureAria
ariaLabel={ariaLabel}
prePopulatedAriaLabel={getPrepopulatedAriaLabel()}
getPrepopulatedAriaLabel={getPrepopulatedAriaLabel}
onChangeProps={(newProps) => {
onChangeProps(newProps);
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ const defaultProps = {

const defaultLabel = getDefaultFigureForType("label");

// Mock the async function generateSpokenMathDetails
jest.mock("./util", () => ({
...jest.requireActual("./util"),
generateSpokenMathDetails: (input) => {
return Promise.resolve(`Spoken math details for ${input}`);
},
}));

describe("LockedPolygonSettings", () => {
let userEvent: UserEvent;
beforeEach(() => {
Expand Down Expand Up @@ -598,7 +606,7 @@ describe("LockedPolygonSettings", () => {
// Assert
expect(onChangeProps).toHaveBeenCalledWith({
ariaLabel:
"Polygon with 3 sides, vertices at (0, 0), (0, 1), (1, 1). Appearance solid gray border, with no fill.",
"Spoken math details for Polygon with 3 sides, vertices at (0, 0), (0, 1), (1, 1). Appearance solid gray border, with no fill.",
});
});

Expand Down Expand Up @@ -634,7 +642,7 @@ describe("LockedPolygonSettings", () => {
// Assert
expect(onChangeProps).toHaveBeenCalledWith({
ariaLabel:
"Polygon A with 3 sides, vertices at (0, 0), (0, 1), (1, 1). Appearance solid gray border, with no fill.",
"Spoken math details for Polygon A with 3 sides, vertices at (0, 0), (0, 1), (1, 1). Appearance solid gray border, with no fill.",
});
});

Expand Down Expand Up @@ -674,7 +682,7 @@ describe("LockedPolygonSettings", () => {
// Assert
expect(onChangeProps).toHaveBeenCalledWith({
ariaLabel:
"Polygon A, B with 3 sides, vertices at (0, 0), (0, 1), (1, 1). Appearance solid gray border, with no fill.",
"Spoken math details for Polygon A, B with 3 sides, vertices at (0, 0), (0, 1), (1, 1). Appearance solid gray border, with no fill.",
});
});
});
Expand Down
Loading

0 comments on commit 5e930ce

Please sign in to comment.