diff --git a/packages/perseus/src/widgets/interactive-graphs/ariaLiveAnnounce.ts b/packages/perseus/src/widgets/interactive-graphs/ariaLiveAnnounce.ts new file mode 100644 index 0000000000..b39635a563 --- /dev/null +++ b/packages/perseus/src/widgets/interactive-graphs/ariaLiveAnnounce.ts @@ -0,0 +1,24 @@ +type PolitenessLevel = "assertive" | "polite"; + +export function ariaLiveAnnounce(message: string, options?: {level?: PolitenessLevel}): void { + const region = recreateAriaLiveRegion(options?.level ?? "polite") + region.innerText = message +} + +let currentAriaLiveRegion: HTMLDivElement | null = null +function recreateAriaLiveRegion(politenessLevel: PolitenessLevel): HTMLDivElement { + if (currentAriaLiveRegion) { + document.body.removeChild(currentAriaLiveRegion) + } + + currentAriaLiveRegion = createAriaLiveRegion(politenessLevel) + return currentAriaLiveRegion +} + +function createAriaLiveRegion(politenessLevel: PolitenessLevel): HTMLDivElement { + console.log("createAriaLiveRegion") + const newRegion = document.createElement("div") + newRegion.setAttribute("aria-live", politenessLevel) + newRegion.classList.add("perseus-aria-live-region") + return document.body.appendChild(newRegion) +} diff --git a/packages/perseus/src/widgets/interactive-graphs/graphs/point.tsx b/packages/perseus/src/widgets/interactive-graphs/graphs/point.tsx index dcb5fa197c..eb955390c4 100644 --- a/packages/perseus/src/widgets/interactive-graphs/graphs/point.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/graphs/point.tsx @@ -19,6 +19,7 @@ import type { Dispatch, InteractiveGraphElementSuite, } from "../types"; +import {ariaLiveAnnounce} from "../ariaLiveAnnounce"; export function renderPointGraph( state: PointGraphState, @@ -42,7 +43,14 @@ function PointGraph(props: PointGraphProps) { } function LimitedPointGraph(props: PointGraphProps) { - const {dispatch} = props; + const {dispatch, graphState} = props; + const {announcement} = graphState; + + React.useEffect(() => { + if (announcement) { + ariaLiveAnnounce(announcement.text, {level: "assertive"}) + } + }, [announcement]) return ( <> diff --git a/packages/perseus/src/widgets/interactive-graphs/reducer/interactive-graph-reducer.ts b/packages/perseus/src/widgets/interactive-graphs/reducer/interactive-graph-reducer.ts index 211a3d21ff..29c9c21069 100644 --- a/packages/perseus/src/widgets/interactive-graphs/reducer/interactive-graph-reducer.ts +++ b/packages/perseus/src/widgets/interactive-graphs/reducer/interactive-graph-reducer.ts @@ -77,6 +77,7 @@ import type { PairOfPoints, } from "../types"; import type {Interval} from "mafs"; +import {ariaLiveAnnounce} from "../ariaLiveAnnounce"; const minDistanceBetweenAngleVertexAndSidePoint = 2; @@ -507,6 +508,7 @@ function doMovePoint( index: action.index, newValue: boundAndSnapToGrid(action.destination, state), }), + announcement: {text: "updated"} }; } case "sinusoid": { diff --git a/packages/perseus/src/widgets/interactive-graphs/types.ts b/packages/perseus/src/widgets/interactive-graphs/types.ts index 140cea9f02..25fc4e3cd6 100644 --- a/packages/perseus/src/widgets/interactive-graphs/types.ts +++ b/packages/perseus/src/widgets/interactive-graphs/types.ts @@ -46,6 +46,7 @@ export interface InteractiveGraphStateCommon { range: [xRange: Interval, yRange: Interval]; // snapStep = [xStep, yStep] in Cartesian units snapStep: vec.Vector2; + announcement?: {text: string}; } export interface SegmentGraphState extends InteractiveGraphStateCommon {