fix: stabilize notebook actions menu

This commit is contained in:
tianma-if committed 2026-07-29 15:48:15 +08:00
1 parent af01870873
commit 1e384d2c0b
4 files changed
+85 -45

No files matched your search

+80 -44
View File
@@ -1,6 +1,6 @@
import { useState, useEffect, useRef, type DragEvent } from "react";
import { useTranslation } from "react-i18next";
import { ChevronDown, ChevronRight, Notebook as NotebookIcon, Plus, Pencil, Trash2 } from "lucide-react";
import { ChevronDown, ChevronRight, MoreHorizontal, Notebook as NotebookIcon, Plus, Pencil, Trash2 } from "lucide-react";
import type { NotebookNode, NotebookDropPosition } from "@/lib/app-helpers";
import {
hasMemoDragData,
@@ -58,6 +58,8 @@ export const NotebookTreeItem = ({
const isInbox = node.slug === "inbox";
const hasSelectedDescendant = selectedNotebookId ? notebookTreeContainsId(node.children, selectedNotebookId) : false;
const [dropPosition, setDropPosition] = useState<NotebookDropPosition | null>(null);
const [actionsOpen, setActionsOpen] = useState(false);
const actionsRef = useRef<HTMLDivElement>(null);
const expandTimerRef = useRef<number | null>(null);
const clearExpandTimer = () => {
@@ -71,6 +73,21 @@ export const NotebookTreeItem = ({
useEffect(() => () => clearExpandTimer(), []);
useEffect(() => {
if (!actionsOpen) {
return;
}
const closeOnOutsidePointer = (event: PointerEvent) => {
if (!actionsRef.current?.contains(event.target as Node)) {
setActionsOpen(false);
}
};
document.addEventListener("pointerdown", closeOnOutsidePointer);
return () => document.removeEventListener("pointerdown", closeOnOutsidePointer);
}, [actionsOpen]);
useEffect(() => {
if (hasSelectedDescendant) {
setOpen(true);
@@ -256,50 +273,69 @@ export const NotebookTreeItem = ({
{node.memoCount}
</span>
</button>
<button
className={cn(
"hidden h-6 w-6 items-center justify-center rounded-md group-focus-within:flex group-hover:flex transition-colors duration-150",
selected ? "hover:bg-slate-200" : "hover:bg-slate-100"
<div ref={actionsRef} className="relative shrink-0">
<button
className={cn(
"hidden h-6 w-6 items-center justify-center rounded-md group-focus-within:flex group-hover:flex transition-colors duration-150",
selected ? "hover:bg-slate-200" : "hover:bg-slate-100"
)}
type="button"
title={t("notebookTree.actions")}
aria-label={t("notebookTree.actionsAria", { name: node.name })}
aria-expanded={actionsOpen}
onClick={(event) => {
event.stopPropagation();
setActionsOpen((openValue) => !openValue);
}}
>
<MoreHorizontal className="h-3.5 w-3.5" />
</button>
{actionsOpen && (
<div className="absolute right-0 top-8 z-50 w-44 overflow-hidden rounded-md border border-slate-200 bg-white p-1 text-slate-950 shadow-lg">
<button
className="flex h-9 w-full items-center gap-2 rounded-sm px-2 text-left text-sm outline-none hover:bg-slate-100"
type="button"
onClick={(event) => {
event.stopPropagation();
setActionsOpen(false);
onCreateNotebook(node.id);
}}
>
<Plus className="h-4 w-4" />
{t("notebookTree.newChild")}
</button>
<button
className="flex h-9 w-full items-center gap-2 rounded-sm px-2 text-left text-sm outline-none hover:bg-slate-100"
type="button"
onClick={(event) => {
event.stopPropagation();
setActionsOpen(false);
onRenameNotebook(node);
}}
>
<Pencil className="h-4 w-4" />
{t("notebookTree.rename")}
</button>
{!isInbox && (
<>
<div className="-mx-1 my-1 h-px bg-slate-100" />
<button
className="flex h-9 w-full items-center gap-2 rounded-sm px-2 text-left text-sm text-rose-700 outline-none hover:bg-rose-50"
type="button"
onClick={(event) => {
event.stopPropagation();
setActionsOpen(false);
onDeleteNotebook(node);
}}
>
<Trash2 className="h-4 w-4" />
{t("notebookTree.deleteNotebook")}
</button>
</>
)}
</div>
)}
type="button"
title={t("notebookTree.newChild")}
aria-label={t("notebookTree.newChildAria", { name: node.name })}
onClick={(event) => {
event.stopPropagation();
onCreateNotebook(node.id);
}}
>
<Plus className="h-3.5 w-3.5" />
</button>
<button
className={cn(
"hidden h-6 w-6 items-center justify-center rounded-md group-focus-within:flex group-hover:flex transition-colors duration-150",
selected ? "hover:bg-slate-200" : "hover:bg-slate-100"
)}
type="button"
title={t("notebookTree.renameNotebook")}
aria-label={t("notebookTree.renameAria", { name: node.name })}
onClick={(event) => {
event.stopPropagation();
onRenameNotebook(node);
}}
>
<Pencil className="h-3.5 w-3.5" />
</button>
{!isInbox ? (
<button
className="hidden h-6 w-6 items-center justify-center rounded-md text-rose-600 hover:bg-rose-50 group-focus-within:flex group-hover:flex transition-colors duration-150"
type="button"
title={t("notebookTree.deleteNotebook")}
aria-label={t("notebookTree.deleteAria", { name: node.name })}
onClick={(event) => {
event.stopPropagation();
onDeleteNotebook(node);
}}
>
<Trash2 className="h-3.5 w-3.5" />
</button>
) : null}
</div>
{dropPosition === "before" && (
<div className="absolute top-0 right-2 h-[3px] bg-slate-400 rounded-full z-30 animate-pulse" style={{ left: `${20 + depth * 14}px` }} />
)}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "edgeever",
"version": "1.6.48",
"version": "1.6.49",
"private": true,
"type": "module",
"workspaces": [
+2
View File
@@ -154,6 +154,8 @@ export const enUS = {
switchTo: "Switch to {{name}}",
currentWithCount: "Current: {{name}}, {{count}} notes",
switchToWithCount: "Switch to {{name}}, {{count}} notes",
actions: "Notebook actions",
actionsAria: "Open actions for {{name}}",
newChild: "New child notebook",
newChildAria: "Create child notebook under {{name}}",
rename: "Rename",
+2
View File
@@ -154,6 +154,8 @@ export const zhCN = {
switchTo: "切换到 {{name}}",
currentWithCount: "当前:{{name}},{{count}} 条笔记",
switchToWithCount: "切换到 {{name}},{{count}} 条笔记",
actions: "笔记本操作",
actionsAria: "打开 {{name}} 的操作菜单",
newChild: "新建子笔记本",
newChildAria: "在 {{name}} 下新建子笔记本",
rename: "重命名",