From 1251751ed3ddaddaa767b2baeea9a3edbb3b432c Mon Sep 17 00:00:00 2001 From: abhay-codes07 Date: Fri, 24 Jul 2026 11:38:10 +0530 Subject: [PATCH] fix(memory-graph): accessible names and states for graph controls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The graph control surfaces had a few screen-reader gaps in the published component: - The zoom in/out buttons rendered only a "+"/"−" glyph with no text, so they had no accessible name. Added aria-label ("Zoom in"/"Zoom out") and marked the glyphs aria-hidden. - The Legend header and the expandable "Connections" row are disclosure buttons but never exposed aria-expanded, so assistive tech could not tell whether the section was open. Wired aria-expanded to the actual state. - The loading indicator was a plain div, so the loading message was not announced. Switched it to an output element (implicit status role) with aria-live="polite". Adds a mounted test covering the accessible names, the toggling aria-expanded, and the loading live region. --- .../src/__tests__/controls-a11y.e2e.test.tsx | 95 +++++++++++++++++++ .../memory-graph/src/components/legend.tsx | 2 + .../src/components/loading-indicator.tsx | 4 +- .../src/components/navigation-controls.tsx | 10 +- 4 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 packages/memory-graph/src/__tests__/controls-a11y.e2e.test.tsx diff --git a/packages/memory-graph/src/__tests__/controls-a11y.e2e.test.tsx b/packages/memory-graph/src/__tests__/controls-a11y.e2e.test.tsx new file mode 100644 index 000000000..8fb57293a --- /dev/null +++ b/packages/memory-graph/src/__tests__/controls-a11y.e2e.test.tsx @@ -0,0 +1,95 @@ +/** + * Mounted-render accessibility checks for the graph control surfaces: + * accessible names on icon-only zoom buttons, disclosure state on the legend, + * and a live region on the loading indicator. + */ + +// @vitest-environment happy-dom + +import { cleanup, fireEvent, render } from "@testing-library/react" +import { afterEach, describe, expect, it } from "vitest" + +afterEach(cleanup) + +import { Legend } from "../components/legend" +import { LoadingIndicator } from "../components/loading-indicator" +import { NavigationControls } from "../components/navigation-controls" +import { DEFAULT_COLORS, DEFAULT_LABELS } from "../constants" +import type { GraphNode } from "../types" + +const node: GraphNode = { + id: "doc-1", + type: "document", + x: 0, + y: 0, + data: { + id: "doc-1", + title: "Doc", + summary: null, + type: "text", + createdAt: "2026-01-01", + updatedAt: "2026-01-01", + memories: [], + }, + size: 40, + borderColor: "#fff", + isHovered: false, + isDragging: false, +} + +const noop = () => {} + +describe("NavigationControls accessibility", () => { + it("gives the icon-only zoom buttons accessible names", () => { + const { getByLabelText } = render( + , + ) + + expect(getByLabelText("Zoom in")).toBeTruthy() + expect(getByLabelText("Zoom out")).toBeTruthy() + }) +}) + +describe("Legend accessibility", () => { + it("exposes disclosure state via aria-expanded and toggles it", () => { + const { getByRole } = render( + , + ) + + const toggle = getByRole("button", { name: "Legend" }) + expect(toggle.getAttribute("aria-expanded")).toBe("false") + + fireEvent.click(toggle) + expect(toggle.getAttribute("aria-expanded")).toBe("true") + }) +}) + +describe("LoadingIndicator accessibility", () => { + it("announces loading through a status live region", () => { + const { getByRole } = render( + , + ) + + const status = getByRole("status") + expect(status.getAttribute("aria-live")).toBe("polite") + }) +}) diff --git a/packages/memory-graph/src/components/legend.tsx b/packages/memory-graph/src/components/legend.tsx index e9be3a228..be3267f6a 100644 --- a/packages/memory-graph/src/components/legend.tsx +++ b/packages/memory-graph/src/components/legend.tsx @@ -244,6 +244,7 @@ function StatRow({ onClick={expandable ? onToggle : undefined} style={buttonStyle} type="button" + aria-expanded={expandable ? expanded : undefined} >
{icon} @@ -428,6 +429,7 @@ export const Legend = memo(function Legend({ onClick={() => setIsExpanded(!isExpanded)} style={headerBtnStyle} type="button" + aria-expanded={isExpanded} > {isExpanded ? ( diff --git a/packages/memory-graph/src/components/loading-indicator.tsx b/packages/memory-graph/src/components/loading-indicator.tsx index 8d550eb98..596f9dbdb 100644 --- a/packages/memory-graph/src/components/loading-indicator.tsx +++ b/packages/memory-graph/src/components/loading-indicator.tsx @@ -76,7 +76,7 @@ export const LoadingIndicator = memo< } return ( -
+
-
+ ) }, ) diff --git a/packages/memory-graph/src/components/navigation-controls.tsx b/packages/memory-graph/src/components/navigation-controls.tsx index 2eec6a81a..d17cdc9c8 100644 --- a/packages/memory-graph/src/components/navigation-controls.tsx +++ b/packages/memory-graph/src/components/navigation-controls.tsx @@ -168,6 +168,7 @@ export const NavigationControls = memo( onClick={onZoomOut} style={zoomBtnStyle} type="button" + aria-label="Zoom out" onMouseEnter={(e) => { e.currentTarget.style.opacity = "0.8" }} @@ -175,12 +176,15 @@ export const NavigationControls = memo( e.currentTarget.style.opacity = "1" }} > - +