fix(memory-graph): accessible names and states for graph controls (#1349)

This commit is contained in:
abhay-codes07 2026-10-12 21:42:25 -07:00 • committed by Mahesh Sanikommu
parent 7bfe0b6be4
commit d928490161
4 changed files with 99 additions and 4 deletions

View file

@ -0,0 +1,89 @@
// @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(
<NavigationControls
onCenter={noop}
onZoomIn={noop}
onZoomOut={noop}
onAutoFit={noop}
nodes={[node]}
zoomLevel={100}
colors={DEFAULT_COLORS}
/>,
)
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(
<Legend
nodes={[node]}
edges={[]}
colors={DEFAULT_COLORS}
labels={DEFAULT_LABELS}
/>,
)
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(
<LoadingIndicator
isLoading
isLoadingMore={false}
totalLoaded={0}
colors={DEFAULT_COLORS}
labels={DEFAULT_LABELS}
/>,
)
const status = getByRole("status")
expect(status.getAttribute("aria-live")).toBe("polite")
})
})

View file

@ -244,6 +244,7 @@ function StatRow({
onClick={expandable ? onToggle : undefined}
style={buttonStyle}
type="button"
aria-expanded={expandable ? expanded : undefined}
>
<div style={leftStyle}>
{icon}
@ -430,6 +431,7 @@ export const Legend = memo(function Legend({
onClick={() => setIsExpanded(!isExpanded)}
style={headerBtnStyle}
type="button"
aria-expanded={isExpanded}
>
{isExpanded ? (
<ChevronDownIcon color={colors.textPrimary} />

View file

@ -76,7 +76,7 @@ export const LoadingIndicator = memo<
}
return (
<div style={containerStyle}>
<output style={containerStyle} aria-live="polite">
<div style={flexStyle}>
<svg
aria-hidden="true"
@ -96,7 +96,7 @@ export const LoadingIndicator = memo<
: labels.loadingMoreDocuments(totalLoaded)}
</span>
</div>
</div>
</output>
)
},
)

View file

@ -176,7 +176,9 @@ export const NavigationControls = memo<NavigationControlsProps>(
e.currentTarget.style.opacity = "1"
}}
>
<span style={{ fontSize: 12 }}>−</span>
<span aria-hidden="true" style={{ fontSize: 12 }}>
−
</span>
</button>
<button
aria-label="Zoom in"
@ -190,7 +192,9 @@ export const NavigationControls = memo<NavigationControlsProps>(
e.currentTarget.style.opacity = "1"
}}
>
<span style={{ fontSize: 12 }}>+</span>
<span aria-hidden="true" style={{ fontSize: 12 }}>
+
</span>
</button>
</div>
</div>