fix(web): prevent file copy button overlap (#1355)

This commit is contained in:
Ananovo
2026-08-04 08:03:36 +08:00
committed by GitHub
parent f44c9ff3e6
commit 951091fb3d
2 changed files with 60 additions and 20 deletions
+13 -1
View File
@@ -6,6 +6,7 @@ import { encodeBase64 } from '@/lib/utils'
import FilePage from './file'
const goBackMock = vi.fn()
const copyMock = vi.hoisted(() => vi.fn())
const sampleMarkdown = '# Heading\n\n| Col A | Col B |\n| --- | --- |\n| one | two |'
const filePath = 'docs/README.md'
@@ -39,7 +40,7 @@ vi.mock('@/hooks/useAppGoBack', () => ({
vi.mock('@/hooks/useCopyToClipboard', () => ({
useCopyToClipboard: () => ({
copied: false,
copy: vi.fn(),
copy: copyMock,
}),
}))
@@ -81,6 +82,11 @@ describe('FilePage markdown preview', () => {
await waitFor(() => {
expect(screen.getByTestId('markdown-preview')).toHaveTextContent('# Heading')
})
const previewCopyButton = screen.getByRole('button', { name: 'Copy file content' })
expect(previewCopyButton.closest('[data-hapi-file-content-header="true"]')).not.toBeNull()
expect(previewCopyButton).not.toHaveClass('absolute')
fireEvent.click(previewCopyButton)
expect(copyMock).toHaveBeenCalledWith(sampleMarkdown)
expect(screen.getByRole('button', { name: 'Preview' })).toHaveClass('opacity-80')
fireEvent.click(screen.getByRole('button', { name: 'Source' }))
@@ -88,6 +94,12 @@ describe('FilePage markdown preview', () => {
await waitFor(() => {
expect(screen.getByRole('code')).toHaveTextContent('# Heading')
})
const sourcePreview = screen.getByRole('code').closest('[data-hapi-file-source-preview="true"]')
const sourceCopyButton = screen.getByRole('button', { name: 'Copy file content' })
expect(sourcePreview).not.toBeNull()
expect(sourcePreview).toContainElement(sourceCopyButton)
expect(sourceCopyButton.closest('[data-hapi-file-content-header="true"]')).not.toBeNull()
expect(sourceCopyButton).not.toHaveClass('absolute')
expect(screen.queryByTestId('markdown-preview')).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'Preview' }))
+47 -19
View File
@@ -134,6 +134,33 @@ function FileContentSkeleton(props: { label: string }) {
)
}
function FileContentHeader(props: {
label: string
copied: boolean
copyLabel: string
onCopy: () => void
}) {
return (
<div
data-hapi-file-content-header="true"
className="flex items-center justify-between gap-3 bg-[var(--app-code-header-bg)] px-3 py-2"
>
<div className="min-w-0 flex-1 truncate font-mono text-[11px] uppercase tracking-[0.08em] text-[var(--app-code-header-fg)]">
{props.label}
</div>
<button
type="button"
onClick={props.onCopy}
className="shrink-0 rounded-md p-1 text-[var(--app-code-header-fg)] transition-colors hover:bg-[var(--app-code-copy-hover-bg)] hover:text-[var(--app-fg)]"
title={props.copyLabel}
aria-label={props.copyLabel}
>
{props.copied ? <CheckIcon className="h-3.5 w-3.5" /> : <CopyIcon className="h-3.5 w-3.5" />}
</button>
</div>
)
}
function resolveLanguage(path: string): string | undefined {
const parts = path.split('.')
if (parts.length <= 1) return undefined
@@ -390,32 +417,33 @@ export default function FilePage() {
) : (
decodedContent ? (
markdownFile && !showMarkdownSource ? (
<div className="markdown-content relative">
<div className="markdown-content">
{canCopyContent ? (
<button
type="button"
onClick={() => copyContent(decodedContent)}
className="absolute right-2 top-2 z-10 rounded p-1 text-[var(--app-hint)] hover:bg-[var(--app-subtle-bg)] hover:text-[var(--app-fg)] transition-colors"
title={t('file.page.copyContent')}
>
{contentCopied ? <CheckIcon className="h-3.5 w-3.5" /> : <CopyIcon className="h-3.5 w-3.5" />}
</button>
<div className="mb-3 overflow-hidden rounded-md">
<FileContentHeader
label={t('file.page.tab.preview')}
copied={contentCopied}
copyLabel={t('file.page.copyContent')}
onCopy={() => copyContent(decodedContent)}
/>
</div>
) : null}
<MarkdownRenderer content={decodedContent} standalone />
</div>
) : (
<div className="relative">
<div
data-hapi-file-source-preview="true"
className="min-w-0 max-w-full overflow-hidden rounded-md bg-[var(--app-code-bg)]"
>
{canCopyContent ? (
<button
type="button"
onClick={() => copyContent(decodedContent)}
className="absolute right-2 top-2 z-10 rounded p-1 text-[var(--app-hint)] hover:bg-[var(--app-subtle-bg)] hover:text-[var(--app-fg)] transition-colors"
title={t('file.page.copyContent')}
>
{contentCopied ? <CheckIcon className="h-3.5 w-3.5" /> : <CopyIcon className="h-3.5 w-3.5" />}
</button>
<FileContentHeader
label={language ?? 'text'}
copied={contentCopied}
copyLabel={t('file.page.copyContent')}
onCopy={() => copyContent(decodedContent)}
/>
) : null}
<pre className="shiki overflow-auto rounded-md bg-[var(--app-code-bg)] p-3 pr-8 text-xs font-mono">
<pre className="shiki m-0 overflow-auto bg-[var(--app-code-bg)] p-3 text-xs font-mono">
<code>{highlighted ?? decodedContent}</code>
</pre>
</div>