From 2c402198c00b1eb342a799820a58d49ec5d9d9f4 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Thu, 31 Oct 2024 15:15:22 -0700 Subject: [PATCH] Let interactive graph components render a screenreader description (#1815) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This refactoring will make it easy for each graph subtype (segment, point, linear-system, etc.) to describe itself to a screenreader. At this point, no graph actually renders a screenreader description. Future PRs will add descriptions for each graph type. Issue: https://khanacademy.atlassian.net/browse/LEMS-1725 ## Test plan: `yarn test` Author: benchristel Reviewers: nishasy, benchristel, #perseus, anakaren-rojas, catandthemachines Required Reviewers: Approved By: nishasy Checks: ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ 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), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1815 --- .changeset/chatty-gorillas-shout.md | 5 ++ .../interactive-graphs/graphs/angle.tsx | 19 ++++++- .../interactive-graphs/graphs/circle.tsx | 19 ++++++- .../interactive-graphs/graphs/index.ts | 9 --- .../graphs/linear-system.tsx | 19 ++++++- .../interactive-graphs/graphs/linear.tsx | 19 ++++++- .../interactive-graphs/graphs/point.tsx | 35 ++++++++---- .../interactive-graphs/graphs/polygon.tsx | 23 ++++++-- .../interactive-graphs/graphs/quadratic.tsx | 19 ++++++- .../widgets/interactive-graphs/graphs/ray.tsx | 19 ++++++- .../interactive-graphs/graphs/segment.tsx | 19 ++++++- .../interactive-graphs/graphs/sinusoid.tsx | 19 ++++++- .../widgets/interactive-graphs/mafs-graph.tsx | 56 +++++++++---------- .../src/widgets/interactive-graphs/types.ts | 24 ++++---- 14 files changed, 223 insertions(+), 81 deletions(-) create mode 100644 .changeset/chatty-gorillas-shout.md delete mode 100644 packages/perseus/src/widgets/interactive-graphs/graphs/index.ts diff --git a/.changeset/chatty-gorillas-shout.md b/.changeset/chatty-gorillas-shout.md new file mode 100644 index 0000000000..6484ed0fb4 --- /dev/null +++ b/.changeset/chatty-gorillas-shout.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +Internal: Refactor interactive graph components to support whole-graph screenreader descriptions diff --git a/packages/perseus/src/widgets/interactive-graphs/graphs/angle.tsx b/packages/perseus/src/widgets/interactive-graphs/graphs/angle.tsx index cadd5a57fe..c67881d1e5 100644 --- a/packages/perseus/src/widgets/interactive-graphs/graphs/angle.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/graphs/angle.tsx @@ -16,11 +16,26 @@ import {getIntersectionOfRayWithBox} from "./utils"; import type {CollinearTuple} from "../../../perseus-types"; import type {Segment} from "../math/geometry"; -import type {AngleGraphState, MafsGraphProps} from "../types"; +import type { + AngleGraphState, + Dispatch, + InteractiveGraphElementSuite, + MafsGraphProps, +} from "../types"; type AngleGraphProps = MafsGraphProps; -export function AngleGraph(props: AngleGraphProps) { +export function renderAngleGraph( + state: AngleGraphState, + dispatch: Dispatch, +): InteractiveGraphElementSuite { + return { + graph: , + screenreaderDescription: null, + }; +} + +function AngleGraph(props: AngleGraphProps) { const {dispatch, graphState} = props; const {graphDimensionsInPixels} = useGraphConfig(); diff --git a/packages/perseus/src/widgets/interactive-graphs/graphs/circle.tsx b/packages/perseus/src/widgets/interactive-graphs/graphs/circle.tsx index 97c985f4dd..45b0f23f5c 100644 --- a/packages/perseus/src/widgets/interactive-graphs/graphs/circle.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/graphs/circle.tsx @@ -14,11 +14,26 @@ import { useTransformVectorsToPixels, } from "./use-transform"; -import type {CircleGraphState, MafsGraphProps} from "../types"; +import type { + CircleGraphState, + Dispatch, + InteractiveGraphElementSuite, + MafsGraphProps, +} from "../types"; + +export function renderCircleGraph( + state: CircleGraphState, + dispatch: Dispatch, +): InteractiveGraphElementSuite { + return { + graph: , + screenreaderDescription: null, + }; +} type CircleGraphProps = MafsGraphProps; -export function CircleGraph(props: CircleGraphProps) { +function CircleGraph(props: CircleGraphProps) { const {dispatch, graphState} = props; const {center, radiusPoint} = graphState; diff --git a/packages/perseus/src/widgets/interactive-graphs/graphs/index.ts b/packages/perseus/src/widgets/interactive-graphs/graphs/index.ts deleted file mode 100644 index b7882991de..0000000000 --- a/packages/perseus/src/widgets/interactive-graphs/graphs/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export {SegmentGraph} from "./segment"; -export {LinearGraph} from "./linear"; -export {LinearSystemGraph} from "./linear-system"; -export {RayGraph} from "./ray"; -export {PolygonGraph} from "./polygon"; -export {CircleGraph} from "./circle"; -export {QuadraticGraph} from "./quadratic"; -export {SinusoidGraph} from "./sinusoid"; -export {AngleGraph} from "./angle"; diff --git a/packages/perseus/src/widgets/interactive-graphs/graphs/linear-system.tsx b/packages/perseus/src/widgets/interactive-graphs/graphs/linear-system.tsx index 0513ea41ff..292cbeb57e 100644 --- a/packages/perseus/src/widgets/interactive-graphs/graphs/linear-system.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/graphs/linear-system.tsx @@ -4,12 +4,27 @@ import {actions} from "../reducer/interactive-graph-action"; import {MovableLine} from "./components/movable-line"; -import type {MafsGraphProps, LinearSystemGraphState} from "../types"; +import type { + MafsGraphProps, + LinearSystemGraphState, + Dispatch, + InteractiveGraphElementSuite, +} from "../types"; import type {vec} from "mafs"; +export function renderLinearSystemGraph( + state: LinearSystemGraphState, + dispatch: Dispatch, +): InteractiveGraphElementSuite { + return { + graph: , + screenreaderDescription: null, + }; +} + type LinearSystemGraphProps = MafsGraphProps; -export const LinearSystemGraph = (props: LinearSystemGraphProps) => { +const LinearSystemGraph = (props: LinearSystemGraphProps) => { const {dispatch} = props; const {coords: lines} = props.graphState; diff --git a/packages/perseus/src/widgets/interactive-graphs/graphs/linear.tsx b/packages/perseus/src/widgets/interactive-graphs/graphs/linear.tsx index 24312440ac..fd0b6539e4 100644 --- a/packages/perseus/src/widgets/interactive-graphs/graphs/linear.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/graphs/linear.tsx @@ -4,12 +4,27 @@ import {actions} from "../reducer/interactive-graph-action"; import {MovableLine} from "./components/movable-line"; -import type {MafsGraphProps, LinearGraphState} from "../types"; +import type { + MafsGraphProps, + LinearGraphState, + Dispatch, + InteractiveGraphElementSuite, +} from "../types"; import type {vec} from "mafs"; +export function renderLinearGraph( + state: LinearGraphState, + dispatch: Dispatch, +): InteractiveGraphElementSuite { + return { + graph: , + screenreaderDescription: null, + }; +} + type LinearGraphProps = MafsGraphProps; -export const LinearGraph = (props: LinearGraphProps, key: number) => { +const LinearGraph = (props: LinearGraphProps, key: number) => { const {dispatch} = props; const {coords: line} = props.graphState; diff --git a/packages/perseus/src/widgets/interactive-graphs/graphs/point.tsx b/packages/perseus/src/widgets/interactive-graphs/graphs/point.tsx index e3718434ea..f17d4661a0 100644 --- a/packages/perseus/src/widgets/interactive-graphs/graphs/point.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/graphs/point.tsx @@ -10,10 +10,34 @@ import { pixelsToVectors, } from "./use-transform"; -import type {PointGraphState, MafsGraphProps} from "../types"; +import type { + PointGraphState, + MafsGraphProps, + Dispatch, + InteractiveGraphElementSuite, +} from "../types"; + +export function renderPointGraph( + state: PointGraphState, + dispatch: Dispatch, +): InteractiveGraphElementSuite { + return { + graph: , + screenreaderDescription: null, + }; +} type PointGraphProps = MafsGraphProps; +function PointGraph(props: PointGraphProps) { + const numPoints = props.graphState.numPoints; + if (numPoints === "unlimited") { + return UnlimitedPointGraph(props); + } + + return LimitedPointGraph(props); +} + function LimitedPointGraph(props: PointGraphProps) { const {dispatch} = props; @@ -103,12 +127,3 @@ function UnlimitedPointGraph(props: PointGraphProps) { ); } - -export function PointGraph(props: PointGraphProps) { - const numPoints = props.graphState.numPoints; - if (numPoints === "unlimited") { - return UnlimitedPointGraph(props); - } - - return LimitedPointGraph(props); -} diff --git a/packages/perseus/src/widgets/interactive-graphs/graphs/polygon.tsx b/packages/perseus/src/widgets/interactive-graphs/graphs/polygon.tsx index 8d21862cf2..3e936d1697 100644 --- a/packages/perseus/src/widgets/interactive-graphs/graphs/polygon.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/graphs/polygon.tsx @@ -13,11 +13,26 @@ import {useDraggable} from "./use-draggable"; import {pixelsToVectors, useTransformVectorsToPixels} from "./use-transform"; import type {CollinearTuple} from "../../../perseus-types"; -import type {MafsGraphProps, PolygonGraphState} from "../types"; +import type { + Dispatch, + InteractiveGraphElementSuite, + MafsGraphProps, + PolygonGraphState, +} from "../types"; + +export function renderPolygonGraph( + state: PolygonGraphState, + dispatch: Dispatch, +): InteractiveGraphElementSuite { + return { + graph: , + screenreaderDescription: null, + }; +} type Props = MafsGraphProps; -export const LimitedPolygonGraph = (props: Props) => { +const LimitedPolygonGraph = (props: Props) => { const [hovered, setHovered] = React.useState(false); // This is more so required for the re-rendering that occurs when state // updates; specifically with regard to line weighting and polygon focus. @@ -157,7 +172,7 @@ export const LimitedPolygonGraph = (props: Props) => { // TODO(catjohnson): reduce redundancy between LimitedPolygonGraph and UnlimitedPolygonGraph // both components are vary similar, however more implementation is needed to be added before // it is clear what can and can't be shared between components. -export const UnlimitedPolygonGraph = (props: Props) => { +const UnlimitedPolygonGraph = (props: Props) => { const [hovered, setHovered] = React.useState(false); // This is more so required for the re-rendering that occurs when state // updates; specifically with regard to line weighting and polygon focus. @@ -369,7 +384,7 @@ export const hasFocusVisible = ( } }; -export const PolygonGraph = (props: Props) => { +const PolygonGraph = (props: Props) => { const numSides = props.graphState.numSides; return numSides === "unlimited" diff --git a/packages/perseus/src/widgets/interactive-graphs/graphs/quadratic.tsx b/packages/perseus/src/widgets/interactive-graphs/graphs/quadratic.tsx index 746425e164..775e5f9658 100644 --- a/packages/perseus/src/widgets/interactive-graphs/graphs/quadratic.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/graphs/quadratic.tsx @@ -6,13 +6,28 @@ import {actions} from "../reducer/interactive-graph-action"; import {MovablePoint} from "./components/movable-point"; -import type {QuadraticGraphState, MafsGraphProps} from "../types"; +import type { + QuadraticGraphState, + MafsGraphProps, + Dispatch, + InteractiveGraphElementSuite, +} from "../types"; + +export function renderQuadraticGraph( + state: QuadraticGraphState, + dispatch: Dispatch, +): InteractiveGraphElementSuite { + return { + graph: , + screenreaderDescription: null, + }; +} type QuadraticGraphProps = MafsGraphProps; type QuadraticCoefficient = [number, number, number]; export type QuadraticCoords = QuadraticGraphState["coords"]; -export function QuadraticGraph(props: QuadraticGraphProps) { +function QuadraticGraph(props: QuadraticGraphProps) { const {dispatch, graphState} = props; const {coords} = graphState; diff --git a/packages/perseus/src/widgets/interactive-graphs/graphs/ray.tsx b/packages/perseus/src/widgets/interactive-graphs/graphs/ray.tsx index 5a4c57a726..0731d66c71 100644 --- a/packages/perseus/src/widgets/interactive-graphs/graphs/ray.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/graphs/ray.tsx @@ -4,12 +4,27 @@ import {actions} from "../reducer/interactive-graph-action"; import {MovableLine} from "./components/movable-line"; -import type {MafsGraphProps, RayGraphState} from "../types"; +import type { + Dispatch, + InteractiveGraphElementSuite, + MafsGraphProps, + RayGraphState, +} from "../types"; import type {vec} from "mafs"; +export function renderRayGraph( + state: RayGraphState, + dispatch: Dispatch, +): InteractiveGraphElementSuite { + return { + graph: , + screenreaderDescription: null, + }; +} + type Props = MafsGraphProps; -export const RayGraph = (props: Props) => { +const RayGraph = (props: Props) => { const {dispatch} = props; const {coords: line} = props.graphState; diff --git a/packages/perseus/src/widgets/interactive-graphs/graphs/segment.tsx b/packages/perseus/src/widgets/interactive-graphs/graphs/segment.tsx index 09537982c7..7367999a1a 100644 --- a/packages/perseus/src/widgets/interactive-graphs/graphs/segment.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/graphs/segment.tsx @@ -4,12 +4,27 @@ import {actions} from "../reducer/interactive-graph-action"; import {MovableLine} from "./components/movable-line"; -import type {MafsGraphProps, SegmentGraphState} from "../types"; +import type { + Dispatch, + InteractiveGraphElementSuite, + MafsGraphProps, + SegmentGraphState, +} from "../types"; import type {vec} from "mafs"; +export function renderSegmentGraph( + state: SegmentGraphState, + dispatch: Dispatch, +): InteractiveGraphElementSuite { + return { + graph: , + screenreaderDescription: null, + }; +} + type SegmentProps = MafsGraphProps; -export const SegmentGraph = (props: SegmentProps) => { +const SegmentGraph = (props: SegmentProps) => { const {dispatch} = props; const {coords: segments} = props.graphState; diff --git a/packages/perseus/src/widgets/interactive-graphs/graphs/sinusoid.tsx b/packages/perseus/src/widgets/interactive-graphs/graphs/sinusoid.tsx index 46cf219d2c..7f8f709f7f 100644 --- a/packages/perseus/src/widgets/interactive-graphs/graphs/sinusoid.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/graphs/sinusoid.tsx @@ -8,7 +8,22 @@ import {actions} from "../reducer/interactive-graph-action"; import {MovablePoint} from "./components/movable-point"; import type {Coord} from "../../../interactive2/types"; -import type {SinusoidGraphState, MafsGraphProps} from "../types"; +import type { + SinusoidGraphState, + MafsGraphProps, + Dispatch, + InteractiveGraphElementSuite, +} from "../types"; + +export function renderSinusoidGraph( + state: SinusoidGraphState, + dispatch: Dispatch, +): InteractiveGraphElementSuite { + return { + graph: , + screenreaderDescription: null, + }; +} type SinusoidGraphProps = MafsGraphProps; @@ -19,7 +34,7 @@ export type SineCoefficient = { verticalOffset: number; }; -export function SinusoidGraph(props: SinusoidGraphProps) { +function SinusoidGraph(props: SinusoidGraphProps) { const {dispatch, graphState} = props; // Destructure the coordinates from the graph state diff --git a/packages/perseus/src/widgets/interactive-graphs/mafs-graph.tsx b/packages/perseus/src/widgets/interactive-graphs/mafs-graph.tsx index 7041b27cb4..1e7af249b8 100644 --- a/packages/perseus/src/widgets/interactive-graphs/mafs-graph.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/mafs-graph.tsx @@ -26,19 +26,17 @@ import {Grid} from "./backgrounds/grid"; import {LegacyGrid} from "./backgrounds/legacy-grid"; import GraphLockedLabelsLayer from "./graph-locked-labels-layer"; import GraphLockedLayer from "./graph-locked-layer"; -import { - LinearGraph, - LinearSystemGraph, - PolygonGraph, - RayGraph, - SegmentGraph, - CircleGraph, - QuadraticGraph, - SinusoidGraph, - AngleGraph, -} from "./graphs"; +import {renderAngleGraph} from "./graphs/angle"; +import {renderCircleGraph} from "./graphs/circle"; import {SvgDefs} from "./graphs/components/text-label"; -import {PointGraph} from "./graphs/point"; +import {renderLinearGraph} from "./graphs/linear"; +import {renderLinearSystemGraph} from "./graphs/linear-system"; +import {renderPointGraph} from "./graphs/point"; +import {renderPolygonGraph} from "./graphs/polygon"; +import {renderQuadraticGraph} from "./graphs/quadratic"; +import {renderRayGraph} from "./graphs/ray"; +import {renderSegmentGraph} from "./graphs/segment"; +import {renderSinusoidGraph} from "./graphs/sinusoid"; import {MIN, X, Y} from "./math"; import {Protractor} from "./protractor"; import {actions} from "./reducer/interactive-graph-action"; @@ -51,6 +49,7 @@ import type { InteractiveGraphProps, PointGraphState, PolygonGraphState, + InteractiveGraphElementSuite, } from "./types"; import type {PerseusStrings} from "../../strings"; import type {APIOptions} from "../../types"; @@ -130,6 +129,8 @@ export const MafsGraph = (props: MafsGraphProps) => { }); }); + const {graph} = renderGraphElements({state, dispatch}); + return ( { {/* Protractor */} {props.showProtractor && } {/* Interactive layer */} - {renderGraph({ - state, - dispatch, - })} + {graph} @@ -603,35 +601,35 @@ export const calculateNestedSVGCoords = ( }; }; -const renderGraph = (props: { +const renderGraphElements = (props: { state: InteractiveGraphState; dispatch: (action: InteractiveGraphAction) => unknown; -}) => { +}): InteractiveGraphElementSuite => { const {state, dispatch} = props; const {type} = state; switch (type) { case "angle": - return ; + return renderAngleGraph(state, dispatch); case "segment": - return ; + return renderSegmentGraph(state, dispatch); case "linear-system": - return ; + return renderLinearSystemGraph(state, dispatch); case "linear": - return ; + return renderLinearGraph(state, dispatch); case "ray": - return ; + return renderRayGraph(state, dispatch); case "polygon": - return ; + return renderPolygonGraph(state, dispatch); case "point": - return ; + return renderPointGraph(state, dispatch); case "circle": - return ; + return renderCircleGraph(state, dispatch); case "quadratic": - return ; + return renderQuadraticGraph(state, dispatch); case "sinusoid": - return ; + return renderSinusoidGraph(state, dispatch); case "none": - return null; + return {graph: null, screenreaderDescription: null}; default: throw new UnreachableCaseError(type); } diff --git a/packages/perseus/src/widgets/interactive-graphs/types.ts b/packages/perseus/src/widgets/interactive-graphs/types.ts index 12e1aed9a8..05a9df0971 100644 --- a/packages/perseus/src/widgets/interactive-graphs/types.ts +++ b/packages/perseus/src/widgets/interactive-graphs/types.ts @@ -3,29 +3,27 @@ import type {Coord} from "../../interactive2/types"; import type {PerseusInteractiveGraphWidgetOptions} from "../../perseus-types"; import type {WidgetProps} from "../../types"; import type {Interval, vec} from "mafs"; +import type {ReactNode} from "react"; export type InteractiveGraphProps = WidgetProps< PerseusInteractiveGraphWidgetOptions, PerseusInteractiveGraphWidgetOptions >; +export type Dispatch = (action: InteractiveGraphAction) => unknown; + export type MafsGraphProps = { graphState: T; - dispatch: (action: InteractiveGraphAction) => unknown; + dispatch: Dispatch; }; -export type InteractiveGraphType = - | "none" - | "angle" - | "segment" - | "linear-system" - | "linear" - | "ray" - | "polygon" - | "point" - | "circle" - | "quadratic" - | "sinusoid"; +// InteractiveGraphElementSuite contains parts of the graph UI which need to +// end up in different sections of the DOM. +export type InteractiveGraphElementSuite = { + graph: ReactNode; + screenreaderDescription: ReactNode; + // TODO(benchristel): add actionBar controls here +}; export type InteractiveGraphState = | AngleGraphState