From 7a854a8a5ed0c64e83881d57e1214264fa4f5172 Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Fri, 18 Sep 2026 06:09:22 +0000 Subject: [PATCH] fix(memory-graph): ship accessible legend disclosures --- packages/memory-graph/package.json | 2 +- .../src/__tests__/legend.test.tsx | 42 +++++++ .../memory-graph/src/components/legend.tsx | 117 ++++++++++++------ 3 files changed, 123 insertions(+), 38 deletions(-) create mode 100644 packages/memory-graph/src/__tests__/legend.test.tsx diff --git a/packages/memory-graph/package.json b/packages/memory-graph/package.json index aea9f1d6..d6400257 100644 --- a/packages/memory-graph/package.json +++ b/packages/memory-graph/package.json @@ -1,6 +1,6 @@ { "name": "@supermemory/memory-graph", - "version": "0.2.3", + "version": "0.2.4", "description": "Interactive graph visualization component for Supermemory - visualize and explore your memory connections", "type": "module", "main": "./src/index.tsx", diff --git a/packages/memory-graph/src/__tests__/legend.test.tsx b/packages/memory-graph/src/__tests__/legend.test.tsx new file mode 100644 index 00000000..f9cafcb9 --- /dev/null +++ b/packages/memory-graph/src/__tests__/legend.test.tsx @@ -0,0 +1,42 @@ +import { cleanup, fireEvent, render, screen } from "@testing-library/react" +import { afterEach, describe, expect, it } from "vitest" +import { Legend } from "../components/legend" +import { DEFAULT_COLORS } from "../constants" + +afterEach(cleanup) + +describe("Legend", () => { + it("keeps disclosure state and motion behavior in sync", () => { + const { container } = render( + , + ) + + const legendButton = screen.getByRole("button", { name: "Legend" }) + const legendReveal = container.querySelector(".mg-legend-reveal") + + expect(legendButton.getAttribute("aria-expanded")).toBe("false") + expect(legendReveal?.getAttribute("data-expanded")).toBe("false") + expect( + document.getElementById("supermemory-graph-legend-motion")?.textContent, + ).toContain("prefers-reduced-motion") + + fireEvent.click(legendButton) + + expect(legendButton.getAttribute("aria-expanded")).toBe("true") + expect(legendReveal?.getAttribute("data-expanded")).toBe("true") + }) + + it("exposes the connections disclosure state", () => { + render() + fireEvent.click(screen.getByRole("button", { name: "Legend" })) + + const connectionsButton = screen.getByRole("button", { + name: /Connections/, + }) + expect(connectionsButton.getAttribute("aria-expanded")).toBe("true") + + fireEvent.click(connectionsButton) + + expect(connectionsButton.getAttribute("aria-expanded")).toBe("false") + }) +}) diff --git a/packages/memory-graph/src/components/legend.tsx b/packages/memory-graph/src/components/legend.tsx index e9be3a22..648313d7 100644 --- a/packages/memory-graph/src/components/legend.tsx +++ b/packages/memory-graph/src/components/legend.tsx @@ -1,4 +1,4 @@ -import { memo, useState } from "react" +import { memo, useEffect, useState } from "react" import { DEFAULT_LABELS } from "../constants" import type { GraphEdge, @@ -161,25 +161,6 @@ function ChevronDownIcon({ color }: { color: string }) { ) } -function ChevronRightIcon({ color }: { color: string }) { - return ( - - ) -} - function StatRow({ icon, label, @@ -241,6 +222,7 @@ function StatRow({ return (
{count} - {expandable && expanded && children && ( -
{children}
+ {expandable && children && ( + +
{children}
+
)} ) } +const LEGEND_MOTION_STYLE_ID = "supermemory-graph-legend-motion" +const LEGEND_MOTION_STYLES = ` +.mg-legend-reveal { + display: grid; + grid-template-rows: 0fr; + opacity: 0; + transition: + grid-template-rows 200ms cubic-bezier(0.23, 1, 0.32, 1), + opacity 200ms cubic-bezier(0.23, 1, 0.32, 1); +} +.mg-legend-reveal[data-expanded="true"] { + grid-template-rows: 1fr; + opacity: 1; +} +.mg-legend-chevron { + display: inline-flex; + flex-shrink: 0; + transition: transform 200ms cubic-bezier(0.23, 1, 0.32, 1); +} +@media (prefers-reduced-motion: reduce) { + .mg-legend-reveal, + .mg-legend-chevron { + transition: none; + } +} +` + +function LegendChevron({ + color, + expanded, +}: { + color: string + expanded: boolean +}) { + return ( + + + + ) +} + +function LegendReveal({ + expanded, + children, +}: { + expanded: boolean + children: React.ReactNode +}) { + return ( +
+
{children}
+
+ ) +} + function countEdgesByType(edges: GraphEdge[], edgeType: GraphEdge["edgeType"]) { return edges.filter((edge) => edge.edgeType === edgeType).length } @@ -306,6 +345,14 @@ export const Legend = memo(function Legend({ const [isExpanded, setIsExpanded] = useState(false) const [connectionsExpanded, setConnectionsExpanded] = useState(true) + useEffect(() => { + if (document.getElementById(LEGEND_MOTION_STYLE_ID)) return + const style = document.createElement("style") + style.id = LEGEND_MOTION_STYLE_ID + style.textContent = LEGEND_MOTION_STYLES + document.head.appendChild(style) + }, []) + const memoryCount = nodes.filter((n) => n.type === "memory").length const documentCount = nodes.filter((n) => n.type === "document").length const connectionCount = edges.length @@ -322,7 +369,6 @@ export const Legend = memo(function Legend({ const updateNodeCount = nodes.filter(isUpdateMemoryNode).length const outerStyle: React.CSSProperties = { - overflow: "hidden", width: compact ? "min(214px, calc(100vw - 32px))" : 214, maxWidth: "100%", } @@ -406,7 +452,7 @@ export const Legend = memo(function Legend({ } const expandedContentStyle: React.CSSProperties = { - marginTop: 16, + paddingTop: 16, display: "flex", flexDirection: "column", gap: 16, @@ -425,19 +471,16 @@ export const Legend = memo(function Legend({
- {isExpanded && ( +
{/* Statistics section */}
@@ -654,7 +697,7 @@ export const Legend = memo(function Legend({
- )} +