fix: prevent copy notices from being clipped
This commit is contained in:
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,
|
||||
);
|
||||
};
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"');
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user