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:
commit
652fc31d9b
@ -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);
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
83
canvas/src/components/__tests__/ZoomShortcut.test.tsx
Normal file
83
canvas/src/components/__tests__/ZoomShortcut.test.tsx
Normal 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"');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user