From c4bbbcb363a3a4f272524d0e44b5f53f9d53d06c Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Thu, 5 Dec 2024 15:53:18 -0800 Subject: [PATCH] Prototype aria-live for interactive graphs --- .../interactive-graphs/ariaLiveAnnounce.ts | 24 +++++++++++++++++++ .../interactive-graphs/graphs/point.tsx | 10 +++++++- .../reducer/interactive-graph-reducer.ts | 2 ++ .../src/widgets/interactive-graphs/types.ts | 1 + 4 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 packages/perseus/src/widgets/interactive-graphs/ariaLiveAnnounce.ts 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 {