fix(file-manager): restore text selection and copying in Markdown previews (#1518)

This commit is contained in:
ZacharyZcR authored and GitHub committed 2026-09-30 09:45:54 -05:00
1 parent a1f72b9e53
commit 64b9b18fe6
2 files changed
+123 -68

No files matched your search

@@ -48,75 +48,87 @@ export function MarkdownRenderer({
: "px-4 py-2 text-foreground";
return (
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
code({ className, children, ...props }) {
const match = /language-(\w+)/.exec(className || "");
// v9 removed the `inline` flag; a fenced block always carries a
// language- class, an inline span never does.
const inline = !className;
return !inline && match ? (
<SyntaxHighlighter
style={syntaxTheme}
language={match[1]}
PreTag="div"
className="rounded-lg"
{...props}
>
{String(children).replace(/\n$/, "")}
</SyntaxHighlighter>
) : (
<code
className="bg-muted px-1 py-0.5 rounded text-sm font-mono"
{...props}
>
{children}
</code>
);
},
h1: ({ children }) => <h1 className={h1Class}>{children}</h1>,
h2: ({ children }) => <h2 className={h2Class}>{children}</h2>,
h3: ({ children }) => <h3 className={h3Class}>{children}</h3>,
h4: ({ children }) => <h4 className={h4Class}>{children}</h4>,
p: ({ children }) => <p className={pClass}>{children}</p>,
ul: ({ children }) => (
<ul className={`${listClass} list-disc`}>{children}</ul>
),
ol: ({ children }) => (
<ol className={`${listClass} list-decimal`}>{children}</ol>
),
li: ({ children }) => (
<li className="mb-1 text-foreground">{children}</li>
),
blockquote: ({ children }) => (
<blockquote className={quoteClass}>{children}</blockquote>
),
table: ({ children }) => (
<div className={tableWrapClass}>
<table className={tableClass}>{children}</table>
</div>
),
thead: ({ children }) => <thead className="bg-muted">{children}</thead>,
tbody: ({ children }) => <tbody>{children}</tbody>,
tr: ({ children }) => (
<tr className="border-b border-border">{children}</tr>
),
th: ({ children }) => <th className={thClass}>{children}</th>,
td: ({ children }) => <td className={tdClass}>{children}</td>,
a: ({ href, children }) => (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 underline"
>
{children}
</a>
),
<div
className="select-text"
tabIndex={0}
onKeyDown={(event) => {
// Keep native text copying out of the file grid's global shortcuts.
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "c")
event.stopPropagation();
}}
>
{content}
</ReactMarkdown>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
code({ className, children, ...props }) {
const match = /language-(\w+)/.exec(className || "");
// v9 removed the `inline` flag; a fenced block always carries a
// language- class, an inline span never does.
const inline = !className;
return !inline && match ? (
<SyntaxHighlighter
style={syntaxTheme}
language={match[1]}
PreTag="div"
className="rounded-lg"
{...props}
>
{String(children).replace(/\n$/, "")}
</SyntaxHighlighter>
) : (
<code
className="bg-muted px-1 py-0.5 rounded text-sm font-mono"
{...props}
>
{children}
</code>
);
},
h1: ({ children }) => <h1 className={h1Class}>{children}</h1>,
h2: ({ children }) => <h2 className={h2Class}>{children}</h2>,
h3: ({ children }) => <h3 className={h3Class}>{children}</h3>,
h4: ({ children }) => <h4 className={h4Class}>{children}</h4>,
p: ({ children }) => <p className={pClass}>{children}</p>,
ul: ({ children }) => (
<ul className={`${listClass} list-disc`}>{children}</ul>
),
ol: ({ children }) => (
<ol className={`${listClass} list-decimal`}>{children}</ol>
),
li: ({ children }) => (
<li className="mb-1 text-foreground">{children}</li>
),
blockquote: ({ children }) => (
<blockquote className={quoteClass}>{children}</blockquote>
),
table: ({ children }) => (
<div className={tableWrapClass}>
<table className={tableClass}>{children}</table>
</div>
),
thead: ({ children }) => (
<thead className="bg-muted">{children}</thead>
),
tbody: ({ children }) => <tbody>{children}</tbody>,
tr: ({ children }) => (
<tr className="border-b border-border">{children}</tr>
),
th: ({ children }) => <th className={thClass}>{children}</th>,
td: ({ children }) => <td className={tdClass}>{children}</td>,
a: ({ href, children }) => (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 underline"
>
{children}
</a>
),
}}
>
{content}
</ReactMarkdown>
</div>
);
}
@@ -0,0 +1,43 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import "@testing-library/jest-dom/vitest";
import { MarkdownRenderer } from "../../../src/frontend/components/MarkdownRenderer";
afterEach(cleanup);
describe("Markdown preview text copying", () => {
it.each([false, true])(
"supports copying in compact=%s previews",
(compact) => {
const fileShortcut = vi.fn();
document.addEventListener("keydown", fileShortcut);
try {
const { container } = render(
<div className="select-none">
<MarkdownRenderer
compact={compact}
content={"# Preview\n\nCopy this text."}
/>
</div>,
);
const preview = container.querySelector<HTMLElement>(".select-text")!;
preview.focus();
expect(preview).toHaveFocus();
expect(
screen.getByRole("heading", { name: "Preview" }),
).toBeInTheDocument();
for (const modifier of [{ ctrlKey: true }, { metaKey: true }]) {
expect(fireEvent.keyDown(preview, { key: "c", ...modifier })).toBe(
true,
);
}
expect(fileShortcut).not.toHaveBeenCalled();
fireEvent.keyDown(preview, { key: "Escape" });
expect(fileShortcut).toHaveBeenCalledTimes(1);
expect(fireEvent.contextMenu(preview)).toBe(true);
} finally {
document.removeEventListener("keydown", fileShortcut);
}
},
);
});