Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wb-announcer-exponential.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus": patch
---

[Interactive Graph] Use WB Announcer in Exponential graph.
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ function ExponentialGraph(props: ExponentialGraphProps) {
}
orientation="horizontal"
ariaLabel={srExponentialAsymptote}
// Move announcements come from the WB Announcer via
// stateAnnouncement; disable aria-live here to avoid the
// asymptote handle double-announcing.
// TODO(LEMS-4189): Remove ariaLive once aria-live is dropped
// from MovableAsymptote.
ariaLive="off"
>
{coeffs !== undefined && (
<ClipToGraphBounds>
Expand Down Expand Up @@ -153,6 +159,12 @@ function ExponentialGraph(props: ExponentialGraphProps) {
onMove={(destination) =>
dispatch(actions.exponential.movePoint(i, destination))
}
// Move announcements come from the WB Announcer via
// stateAnnouncement; disable aria-live here to avoid
// the focusable handle double-announcing.
// TODO(LEMS-4189): Remove ariaLive once aria-live is
// dropped from useControlPoint.
ariaLive="off"
/>
))}
<SRDescInSVG id={descriptionId}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,73 @@ describe("getAnnouncementText", () => {
});
});

describe("move-exponential-point", () => {
// Coord layout: [point1(0), point2(1)], each with its own label.
it("uses the point-1 label for index 0", () => {
const result = getAnnouncementText(
{
type: "move-exponential-point",
pointIndex: 0,
pointLabel: 1,
x: -1,
y: 4,
},
mockStrings,
"en",
);

expect(result).toBe("Point 1 at -1 comma 4.");
});

it("uses the point-2 label for index 1", () => {
const result = getAnnouncementText(
{
type: "move-exponential-point",
pointIndex: 1,
pointLabel: 2,
x: 3,
y: 7,
},
mockStrings,
"en",
);

expect(result).toBe("Point 2 at 3 comma 7.");
});

// TODO(LEMS-4206): allow custom labels for exponential points so we
// can keep the point-1/point-2 wording.
it("uses the custom label, overriding the point-1/point-2 wording, when one is set", () => {
const result = getAnnouncementText(
{
type: "move-exponential-point",
pointIndex: 0,
pointLabel: "A",
x: -1,
y: 4,
},
mockStrings,
"en",
);

expect(result).toBe("Point A at -1 comma 4.");
});
});

describe("move-exponential-asymptote", () => {
it("returns the horizontal-asymptote label at the new y", () => {
const result = getAnnouncementText(
{type: "move-exponential-asymptote", asymptoteY: -2},
mockStrings,
"en",
);

expect(result).toBe(
"Horizontal asymptote at y equals -2. Use up and down arrow keys to move.",
);
});
});

describe("move-angle-point", () => {
// Coord layout: [endingSide(0), vertex(1), startingSide(2)]. The
// side labels include their coords; the vertex also includes the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ export function getAnnouncementText(
);
case "move-sinusoid-point":
return srSinusoidPointLabel(state, strings, locale);
case "move-exponential-point":
return srExponentialPointLabel(state, strings, locale);
case "move-exponential-asymptote":
return strings.srExponentialAsymptote({
asymptoteY: srFormatNumber(state.asymptoteY, locale),
});
case "move-logarithm-point":
return srLogarithmPointLabel(state, strings, locale);
case "move-logarithm-asymptote":
Expand Down Expand Up @@ -174,6 +180,31 @@ function srSinusoidPointLabel(
: strings.srSinusoidMinPoint(formatted);
}

function srExponentialPointLabel(
state: {
pointIndex: number;
pointLabel: string | number;
x: number;
y: number;
},
strings: PerseusStrings,
locale: string,
): string {
const x = srFormatNumber(state.x, locale);
const y = srFormatNumber(state.y, locale);
// A custom author label overrides the point-1/point-2 semantics, matching
// the static aria-label behavior in exponential.tsx.
// TODO(LEMS-4206): Once we update the translation keys to allow custom labels
// we can remove this block in favor of using the index logic below.
if (typeof state.pointLabel === "string") {
return strings.srPointAtCoordinates({num: state.pointLabel, x, y});
}
// Coord layout in exponential graphs: [point1(0), point2(1)].
return state.pointIndex === 0
? strings.srExponentialPoint1({x, y})
: strings.srExponentialPoint2({x, y});
}

function srLogarithmPointLabel(
state: {
pointIndex: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3285,6 +3285,57 @@ describe("movePoint on an exponential graph", () => {
expect(updated.coords[0]).toEqual([0, 3]);
expect(updated.hasBeenInteractedWith).toBe(false);
});

it("sets stateAnnouncement to a move-exponential-point with the new position", () => {
const state = generateExponentialGraphState();

const updated = interactiveGraphReducer(
state,
actions.exponential.movePoint(0, [-1, 4]),
);

invariant(updated.stateAnnouncement?.type === "move-exponential-point");
expect(updated.stateAnnouncement.pointIndex).toBe(0);
expect(updated.stateAnnouncement.x).toBe(-1);
expect(updated.stateAnnouncement.y).toBe(4);
});

it("carries the custom pointLabel when one is set", () => {
const state = generateExponentialGraphState({pointLabels: ["A", "B"]});

const updated = interactiveGraphReducer(
state,
actions.exponential.movePoint(1, [3, 7]),
);

invariant(updated.stateAnnouncement?.type === "move-exponential-point");
expect(updated.stateAnnouncement.pointIndex).toBe(1);
expect(updated.stateAnnouncement.pointLabel).toBe("B");
});

it("falls back to the numeric default when the pointLabel slot is empty", () => {
const state = generateExponentialGraphState({pointLabels: ["", "B"]});

const updated = interactiveGraphReducer(
state,
actions.exponential.movePoint(0, [-1, 4]),
);

invariant(updated.stateAnnouncement?.type === "move-exponential-point");
expect(updated.stateAnnouncement.pointLabel).toBe(1);
});

it("emits no announcement when the move is rejected", () => {
const state = generateExponentialGraphState();

// Moving point 0 onto point 1's x (2) is rejected.
const updated = interactiveGraphReducer(
state,
actions.exponential.movePoint(0, [2, 4]),
);

expect(updated.stateAnnouncement).toBeUndefined();
});
});

describe("moveCenter on an exponential graph (asymptote)", () => {
Expand Down Expand Up @@ -3361,6 +3412,39 @@ describe("moveCenter on an exponential graph (asymptote)", () => {
// Assert — asymptote moves to y=-2 regardless of the x passed
expect(updated.asymptote).toBe(-2);
});

it("sets stateAnnouncement to a move-exponential-asymptote with the new y", () => {
const state = generateExponentialGraphState();

const updated = interactiveGraphReducer(
state,
actions.exponential.moveCenter([-10, -2]),
);

expect(updated.stateAnnouncement).toEqual({
type: "move-exponential-asymptote",
asymptoteY: -2,
});
});

it("emits no announcement when the asymptote move is rejected", () => {
// Point 0 sits at (0, 5); moving the asymptote to y=5 would place its
// handle on the point, so the move is rejected.
const state = generateExponentialGraphState({
coords: [
[0, 5],
[2, 8],
],
asymptote: 1,
});

const updated = interactiveGraphReducer(
state,
actions.exponential.moveCenter([0, 5]),
);

expect(updated.stateAnnouncement).toBeUndefined();
});
});

function generateLogarithmGraphState(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,16 @@ function doMovePoint(
index: action.index,
newValue: boundDestination,
}),
stateAnnouncement: {
type: "move-exponential-point",
pointIndex: action.index,
pointLabel: resolvePointLabel(
state.pointLabels,
action.index,
),
x: boundDestination[X],
y: boundDestination[Y],
},
};
}
case "logarithm": {
Expand Down Expand Up @@ -973,6 +983,10 @@ function doMoveCenter(
...state,
hasBeenInteractedWith: true,
asymptote: newY,
stateAnnouncement: {
type: "move-exponential-asymptote",
asymptoteY: newY,
},
};
}
case "logarithm": {
Expand Down
19 changes: 19 additions & 0 deletions packages/perseus/src/widgets/interactive-graphs/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,16 @@ type MoveSinusoidPointAnnouncement = {
otherY: number;
};

// Exponential graph: the two control points (indices 0, 1) use
// dedicated point labels chosen by index.
type MoveExponentialPointAnnouncement = {
type: "move-exponential-point";
pointIndex: number;
pointLabel: string | number;
x: number;
y: number;
};

// Logarithm graph: the two control points (indices 0, 1) use
// dedicated point labels chosen by index.
type MoveLogarithmPointAnnouncement = {
Expand All @@ -184,6 +194,13 @@ type MoveLogarithmPointAnnouncement = {
y: number;
};

// Exponential graph: the horizontal asymptote moves vertically, so only
// its y-position is carried.
type MoveExponentialAsymptoteAnnouncement = {
type: "move-exponential-asymptote";
asymptoteY: number;
};

// Tangent graph: the inflection point (index 0) and the second/control
// point (index 1) use different labels, chosen by index — mirroring the
// static aria-labels in tangent.tsx.
Expand Down Expand Up @@ -249,6 +266,8 @@ export type InteractiveGraphStateAnnouncement =
| MoveRayLineAnnouncement
| MoveLinearLineAnnouncement
| MoveSinusoidPointAnnouncement
| MoveExponentialPointAnnouncement
| MoveExponentialAsymptoteAnnouncement
| MoveLogarithmPointAnnouncement
| MoveLogarithmAsymptoteAnnouncement
| MoveTangentPointAnnouncement
Expand Down
Loading