Merge pull request #45 from Molecule-AI/feat/zoom-to-team-shortcut

feat(canvas): Z shortcut + help entry for double-click zoom-to-team
This commit is contained in:
Hongming Wang 2026-04-14 07:19:23 -07:00 committed by GitHub
commit 652fc31d9b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 105 additions and 0 deletions

View File

@ -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);

View File

@ -224,6 +224,7 @@ export function Toolbar() {
<HelpRow shortcut="Right-click" text="Use node actions for expand, duplicate, export, restart, or delete." />
<HelpRow shortcut="Chat" text="If a task is still running, the chat tab resumes that session automatically." />
<HelpRow shortcut="Config" text="Use the Config tab for skills, model, secrets, and runtime settings." />
<HelpRow shortcut="Dbl-click / Z" text="Zoom canvas to fit a team node and all its sub-workspaces." />
</div>
</div>
)}

View File

@ -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"');
});
});