diff --git a/src/components/panel/notes/NoteTreeContextMenu.test.tsx b/src/components/panel/notes/NoteTreeContextMenu.test.tsx new file mode 100644 index 00000000..b0b52a77 --- /dev/null +++ b/src/components/panel/notes/NoteTreeContextMenu.test.tsx @@ -0,0 +1,104 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { useState } from "react"; +import { describe, expect, it, vi } from "vitest"; +import { ContextMenu, ContextMenuTrigger } from "@/components/ui/context-menu"; +import type { NoteTreeNode } from "@/types/notes"; +import NoteTreeContextMenu, { + type NoteTreeMenuLabels, +} from "./NoteTreeContextMenu"; +import NoteTreeItem from "./NoteTreeItem"; + +const labels: NoteTreeMenuLabels = { + open: "Open", + newNote: "New note", + newFolder: "New folder", + rename: "Rename", + moveTo: "Move to", + delete: "Delete", + refresh: "Refresh", + root: "Root", + expandAll: "Expand all", + collapseAll: "Collapse all", +}; + +const baseNode = { + parentId: null, + sortOrder: 0, + updatedAtMs: 1, + children: [], +}; + +describe("NoteTreeContextMenu rename focus", () => { + it.each([ + [ + "note", + { ...baseNode, id: "note-1", kind: "note", name: "Note 1", revision: 1 }, + ], + [ + "folder", + { ...baseNode, id: "folder-1", kind: "folder", name: "Folder 1" }, + ], + ] as const)( + "keeps the %s inline editor focused after choosing Rename", + async (_kind, node) => { + render(); + + screen.getByRole("treeitem").focus(); + fireEvent.contextMenu(screen.getByTestId("notes-tree-trigger")); + + const renameItem = await screen.findByText(labels.rename); + fireEvent.click(renameItem); + + const input = await screen.findByDisplayValue(node.name); + await waitFor(() => { + expect( + document.querySelector('[data-slot="context-menu-content"]'), + ).toBeNull(); + expect(document.activeElement).toBe(input); + }); + }, + ); +}); + +function RenameHarness({ node }: { node: NoteTreeNode }) { + const [editing, setEditing] = useState(false); + + return ( + + + + setEditing(false)} + onDragStartNode={vi.fn()} + onDragOverNode={vi.fn()} + onDropNode={vi.fn()} + onDragEnd={vi.fn()} + /> + + + setEditing(true)} + onMove={vi.fn()} + onDelete={vi.fn()} + onRefresh={vi.fn()} + /> + + ); +} diff --git a/src/components/panel/notes/NoteTreeContextMenu.tsx b/src/components/panel/notes/NoteTreeContextMenu.tsx index ddb0bcfc..ab9fdb7f 100644 --- a/src/components/panel/notes/NoteTreeContextMenu.tsx +++ b/src/components/panel/notes/NoteTreeContextMenu.tsx @@ -1,3 +1,4 @@ +import { useRef } from "react"; import { MdAdd, MdCreateNewFolder, @@ -60,6 +61,7 @@ export default function NoteTreeContextMenu({ onDelete, onRefresh, }: NoteTreeContextMenuProps) { + const preventNextContextMenuAutoFocusRef = useRef(false); const parentId = node?.kind === "folder" ? node.id : (node?.parentId ?? null); const moveTargets = folderTargets.filter( (item) => @@ -67,7 +69,14 @@ export default function NoteTreeContextMenu({ ); return ( - + { + if (!preventNextContextMenuAutoFocusRef.current) return; + preventNextContextMenuAutoFocusRef.current = false; + event.preventDefault(); + }} + > {node?.kind === "note" ? ( onOpen(node)}> @@ -88,7 +97,12 @@ export default function NoteTreeContextMenu({ > ) : null} - onRename(node)}> + { + preventNextContextMenuAutoFocusRef.current = true; + onRename(node); + }} + > {labels.rename}