fix: prevent copy notices from being clipped

This commit is contained in:
tianma-if committed 2026-08-15 22:45:01 +08:00
1 parent 41921ad96f
commit f0d60146cb
6 files changed
+45 -23

No files matched your search

@@ -0,0 +1,26 @@
import type { ReactNode } from "react";
import { createPortal } from "react-dom";
import { cn } from "@/lib/utils";
export const ClipboardCopyNotice = ({
children,
status,
}: {
children: ReactNode;
status: "copied" | "error";
}) => {
if (typeof document === "undefined") return null;
return createPortal(
<div
className={cn(
"fixed bottom-[max(1.25rem,env(safe-area-inset-bottom))] left-1/2 z-[120] inline-flex max-w-[calc(100vw-2rem)] -translate-x-1/2 rounded-md px-3 py-2 text-sm font-medium text-white shadow-lg sm:max-w-xl",
status === "copied" ? "bg-emerald-700" : "bg-rose-600",
)}
role={status === "copied" ? "status" : "alert"}
>
<span className="min-w-0 truncate">{children}</span>
</div>,
document.body,
);
};
+3 -8
View File
@@ -42,6 +42,7 @@ import {
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { GitHubRepositoryLink } from "@/components/GitHubRepositoryLink";
import { ClipboardCopyNotice } from "@/components/ClipboardCopyNotice";
import { Input } from "@/components/ui/input";
import {
Command,
@@ -4273,15 +4274,9 @@ const RichEditorPane = ({
)}
{memoIdCopyNotice && (
<div
className={cn(
"fixed bottom-5 left-1/2 z-[120] max-w-[calc(100vw-2rem)] -translate-x-1/2 truncate rounded-md px-3 py-2 text-sm font-medium text-white shadow-lg",
memoIdCopyNotice.status === "copied" ? "bg-emerald-700" : "bg-rose-600",
)}
role={memoIdCopyNotice.status === "copied" ? "status" : "alert"}
>
<ClipboardCopyNotice status={memoIdCopyNotice.status}>
{t(memoIdCopyNotice.status === "copied" ? "editor.noteIdCopied" : "editor.noteIdCopyFailed", { id: memoIdCopyNotice.id })}
</div>
</ClipboardCopyNotice>
)}
{false && useMobilePlainTextEditor && (
+3 -8
View File
@@ -58,6 +58,7 @@ import {
} from "@/components/ui/dropdown-menu";
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";
import { MemoCard } from "./MemoCard";
import { ClipboardCopyNotice } from "./ClipboardCopyNotice";
import { cn } from "@/lib/utils";
import { WORKSPACE_PAGE_TITLE_CLASSNAME } from "@/lib/workspace-ui";
import type { Notebook, MemoSummary } from "@edgeever/shared";
@@ -1608,15 +1609,9 @@ export const MemoListPane = ({
) : null}
{memoIdCopyNotice && (
<div
className={cn(
"fixed bottom-5 left-1/2 z-[120] max-w-[calc(100vw-2rem)] -translate-x-1/2 truncate rounded-md px-3 py-2 text-sm font-medium text-white shadow-lg",
memoIdCopyNotice.status === "copied" ? "bg-emerald-700" : "bg-rose-600",
)}
role={memoIdCopyNotice.status === "copied" ? "status" : "alert"}
>
<ClipboardCopyNotice status={memoIdCopyNotice.status}>
{t(memoIdCopyNotice.status === "copied" ? "editor.noteIdCopied" : "editor.noteIdCopyFailed", { id: memoIdCopyNotice.id })}
</div>
</ClipboardCopyNotice>
)}
{selectionMode && (
+3 -7
View File
@@ -22,6 +22,7 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { WORKSPACE_PAGE_TITLE_CLASSNAME } from "@/lib/workspace-ui";
import { copyTextToClipboard } from "@/lib/clipboard";
import { ClipboardCopyNotice } from "@/components/ClipboardCopyNotice";
import type { MemoTemplate as SavedMemoTemplate } from "@edgeever/shared";
const TemplateIconAction = ({
@@ -543,17 +544,12 @@ export const TemplatesPane = ({
)}
{templateIdCopyNotice && (
<div
className={`fixed bottom-5 left-1/2 z-[120] max-w-[calc(100vw-2rem)] -translate-x-1/2 truncate rounded-md px-3 py-2 text-sm font-medium text-white shadow-lg ${
templateIdCopyNotice.status === "copied" ? "bg-emerald-700" : "bg-rose-600"
}`}
role={templateIdCopyNotice.status === "copied" ? "status" : "alert"}
>
<ClipboardCopyNotice status={templateIdCopyNotice.status}>
{t(
templateIdCopyNotice.status === "copied" ? "templates.idCopied" : "templates.idCopyFailed",
{ id: templateIdCopyNotice.id },
)}
</div>
</ClipboardCopyNotice>
)}
</div>
</TooltipProvider>
+9
View File
@@ -4,6 +4,8 @@ import { readFileSync } from "node:fs";
const readSource = (path: string) => readFileSync(new URL(path, import.meta.url), "utf8");
const webEditorSource = readSource("../apps/web/src/components/EditorPane.tsx");
const webMemoListSource = readSource("../apps/web/src/components/MemoListPane.tsx");
const webCopyNoticeSource = readSource("../apps/web/src/components/ClipboardCopyNotice.tsx");
const androidDetailSource = readSource("../apps/mobile/src/screens/WorkspaceMemoDetail.tsx");
const iosDetailSource = readSource("../apps/ios/EdgeEver/Features/Workspace/MemoDetailView.swift");
@@ -13,6 +15,13 @@ describe("copy current note ID", () => {
expect(webEditorSource).toContain('"editor.copyNoteId"');
});
test("renders copy feedback at the page root so panes cannot clip it", () => {
expect(webMemoListSource).toContain("<ClipboardCopyNotice");
expect(webEditorSource).toContain("<ClipboardCopyNotice");
expect(webCopyNoticeSource).toContain("createPortal(");
expect(webCopyNoticeSource).toContain("document.body");
});
test("copies the raw memo ID from both native mobile clients", () => {
expect(androidDetailSource).toContain("Clipboard.setStringAsync(memo.id)");
expect(androidDetailSource).toContain('label={canCopyMemoId ? "复制笔记 ID"');
+1
View File
@@ -11,5 +11,6 @@ describe("copy note template ID", () => {
expect(templatesPaneSource).toContain("copyTextToClipboard(template.id)");
expect(templatesPaneSource).toContain('label={t("templates.copyId")}');
expect(templatesPaneSource).toContain('"templates.idCopied"');
expect(templatesPaneSource).toContain("<ClipboardCopyNotice");
});
});