diff --git a/canvas/src/components/Canvas.tsx b/canvas/src/components/Canvas.tsx
index 2cf03f30..0be2f5eb 100644
--- a/canvas/src/components/Canvas.tsx
+++ b/canvas/src/components/Canvas.tsx
@@ -189,6 +189,27 @@ function CanvasInner() {
state.selectNode(null);
}
}
+
+ // Z — keyboard equivalent for double-click zoom-to-team (WCAG 2.1.1)
+ if (e.key === "z" || e.key === "Z") {
+ const tag = (e.target as HTMLElement).tagName;
+ if (
+ tag === "INPUT" ||
+ tag === "TEXTAREA" ||
+ tag === "SELECT" ||
+ (e.target as HTMLElement).isContentEditable
+ )
+ return;
+ const state = useCanvasStore.getState();
+ const selectedId = state.selectedNodeId;
+ if (!selectedId) return;
+ const hasChildren = state.nodes.some((n) => n.data.parentId === selectedId);
+ if (hasChildren) {
+ window.dispatchEvent(
+ new CustomEvent("molecule:zoom-to-team", { detail: { nodeId: selectedId } })
+ );
+ }
+ }
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
diff --git a/canvas/src/components/Toolbar.tsx b/canvas/src/components/Toolbar.tsx
index 11ed276c..5bfec57a 100644
--- a/canvas/src/components/Toolbar.tsx
+++ b/canvas/src/components/Toolbar.tsx
@@ -224,6 +224,7 @@ export function Toolbar() {
+
)}
diff --git a/canvas/src/components/__tests__/ZoomShortcut.test.tsx b/canvas/src/components/__tests__/ZoomShortcut.test.tsx
new file mode 100644
index 00000000..b1521fe6
--- /dev/null
+++ b/canvas/src/components/__tests__/ZoomShortcut.test.tsx
@@ -0,0 +1,83 @@
+// @vitest-environment jsdom
+/**
+ * Tests for the Z keyboard shortcut (zoom-to-team) and help panel entry.
+ */
+import React from "react";
+import { render, screen, fireEvent, cleanup } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+afterEach(() => cleanup());
+
+// ─── Z key handler unit tests (no React needed) ─────────────────────────────
+
+describe("Z key → molecule:zoom-to-team", () => {
+ let dispatchedEvents: CustomEvent[] = [];
+
+ beforeEach(() => {
+ dispatchedEvents = [];
+ window.addEventListener("molecule:zoom-to-team", (e) => {
+ dispatchedEvents.push(e as CustomEvent);
+ });
+ });
+
+ afterEach(() => {
+ window.removeEventListener("molecule:zoom-to-team", () => {});
+ });
+
+ it("does NOT fire when no node is selected", () => {
+ // Simulate store: no selection
+ vi.mock("../../../store/canvas", () => ({
+ useCanvasStore: Object.assign(
+ vi.fn(() => null),
+ {
+ getState: () => ({
+ selectedNodeId: null,
+ nodes: [],
+ contextMenu: null,
+ closeContextMenu: vi.fn(),
+ selectNode: vi.fn(),
+ }),
+ }
+ ),
+ }));
+
+ fireEvent.keyDown(window, { key: "Z" });
+ expect(dispatchedEvents).toHaveLength(0);
+ });
+
+ it("does NOT fire when target is an input element", () => {
+ const input = document.createElement("input");
+ document.body.appendChild(input);
+ fireEvent.keyDown(input, { key: "Z" });
+ expect(dispatchedEvents).toHaveLength(0);
+ document.body.removeChild(input);
+ });
+});
+
+// ─── Help panel text test ────────────────────────────────────────────────────
+
+describe("Toolbar help panel — zoom shortcut entry", () => {
+ it("help panel content mentions double-click / Z gesture", async () => {
+ // Read the source to verify the entry is present (static assertion)
+ const { readFileSync } = await import("fs");
+ const { join } = await import("path");
+ const src = readFileSync(
+ join(__dirname, "../../components/Toolbar.tsx"),
+ "utf8"
+ );
+ expect(src).toContain("Dbl-click");
+ expect(src).toContain("Zoom canvas to fit a team node");
+ });
+
+ it("Canvas.tsx Z key handler guards against input elements", async () => {
+ const { readFileSync } = await import("fs");
+ const { join } = await import("path");
+ const src = readFileSync(
+ join(__dirname, "../../components/Canvas.tsx"),
+ "utf8"
+ );
+ expect(src).toContain('e.key === "z" || e.key === "Z"');
+ expect(src).toContain("molecule:zoom-to-team");
+ expect(src).toContain('tag === "INPUT"');
+ });
+});