mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix(web): prevent file copy button overlap (#1355)
This commit is contained in:
@@ -6,6 +6,7 @@ import { encodeBase64 } from '@/lib/utils'
|
|||||||
import FilePage from './file'
|
import FilePage from './file'
|
||||||
|
|
||||||
const goBackMock = vi.fn()
|
const goBackMock = vi.fn()
|
||||||
|
const copyMock = vi.hoisted(() => vi.fn())
|
||||||
|
|
||||||
const sampleMarkdown = '# Heading\n\n| Col A | Col B |\n| --- | --- |\n| one | two |'
|
const sampleMarkdown = '# Heading\n\n| Col A | Col B |\n| --- | --- |\n| one | two |'
|
||||||
const filePath = 'docs/README.md'
|
const filePath = 'docs/README.md'
|
||||||
@@ -39,7 +40,7 @@ vi.mock('@/hooks/useAppGoBack', () => ({
|
|||||||
vi.mock('@/hooks/useCopyToClipboard', () => ({
|
vi.mock('@/hooks/useCopyToClipboard', () => ({
|
||||||
useCopyToClipboard: () => ({
|
useCopyToClipboard: () => ({
|
||||||
copied: false,
|
copied: false,
|
||||||
copy: vi.fn(),
|
copy: copyMock,
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -81,6 +82,11 @@ describe('FilePage markdown preview', () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId('markdown-preview')).toHaveTextContent('# Heading')
|
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')
|
expect(screen.getByRole('button', { name: 'Preview' })).toHaveClass('opacity-80')
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'Source' }))
|
fireEvent.click(screen.getByRole('button', { name: 'Source' }))
|
||||||
@@ -88,6 +94,12 @@ describe('FilePage markdown preview', () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByRole('code')).toHaveTextContent('# Heading')
|
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()
|
expect(screen.queryByTestId('markdown-preview')).not.toBeInTheDocument()
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'Preview' }))
|
fireEvent.click(screen.getByRole('button', { name: 'Preview' }))
|
||||||
|
|||||||
@@ -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 {
|
function resolveLanguage(path: string): string | undefined {
|
||||||
const parts = path.split('.')
|
const parts = path.split('.')
|
||||||
if (parts.length <= 1) return undefined
|
if (parts.length <= 1) return undefined
|
||||||
@@ -390,32 +417,33 @@ export default function FilePage() {
|
|||||||
) : (
|
) : (
|
||||||
decodedContent ? (
|
decodedContent ? (
|
||||||
markdownFile && !showMarkdownSource ? (
|
markdownFile && !showMarkdownSource ? (
|
||||||
<div className="markdown-content relative">
|
<div className="markdown-content">
|
||||||
{canCopyContent ? (
|
{canCopyContent ? (
|
||||||
<button
|
<div className="mb-3 overflow-hidden rounded-md">
|
||||||
type="button"
|
<FileContentHeader
|
||||||
onClick={() => copyContent(decodedContent)}
|
label={t('file.page.tab.preview')}
|
||||||
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"
|
copied={contentCopied}
|
||||||
title={t('file.page.copyContent')}
|
copyLabel={t('file.page.copyContent')}
|
||||||
>
|
onCopy={() => copyContent(decodedContent)}
|
||||||
{contentCopied ? <CheckIcon className="h-3.5 w-3.5" /> : <CopyIcon className="h-3.5 w-3.5" />}
|
/>
|
||||||
</button>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<MarkdownRenderer content={decodedContent} standalone />
|
<MarkdownRenderer content={decodedContent} standalone />
|
||||||
</div>
|
</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 ? (
|
{canCopyContent ? (
|
||||||
<button
|
<FileContentHeader
|
||||||
type="button"
|
label={language ?? 'text'}
|
||||||
onClick={() => copyContent(decodedContent)}
|
copied={contentCopied}
|
||||||
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"
|
copyLabel={t('file.page.copyContent')}
|
||||||
title={t('file.page.copyContent')}
|
onCopy={() => copyContent(decodedContent)}
|
||||||
>
|
/>
|
||||||
{contentCopied ? <CheckIcon className="h-3.5 w-3.5" /> : <CopyIcon className="h-3.5 w-3.5" />}
|
|
||||||
</button>
|
|
||||||
) : null}
|
) : 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>
|
<code>{highlighted ?? decodedContent}</code>
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user