Skip to content

Commit e9f7cef

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
refactor(emcn): share copy code header appearance
1 parent 316fea0 commit e9f7cef

5 files changed

Lines changed: 80 additions & 11 deletions

File tree

‎apps/sim/app/(interfaces)/chat/components/message/components/markdown-renderer.tsx‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,7 @@ const COMPONENTS = {
8888
<span className='font-sans text-[var(--text-tertiary)] text-xs'>
8989
{codeProps.className?.replace('language-', '') || 'code'}
9090
</span>
91-
<CopyCodeButton
92-
code={extractTextContent(codeContent)}
93-
className='text-[var(--text-tertiary)] hover-hover:bg-[var(--surface-5)] hover-hover:text-[var(--text-secondary)]'
94-
/>
91+
<CopyCodeButton code={extractTextContent(codeContent)} appearance='code-header' />
9592
</div>
9693
<pre className='overflow-x-auto bg-[var(--surface-5)] p-4 font-mono text-[var(--text-primary)]'>
9794
{codeContent}

‎apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -315,10 +315,7 @@ const MARKDOWN_COMPONENTS = {
315315
<div className='not-prose my-6 overflow-hidden rounded-lg border border-[var(--border)]'>
316316
<div className='flex items-center justify-between border-[var(--border)] border-b bg-[var(--surface-4)] px-4 py-2 dark:bg-[var(--surface-4)]'>
317317
<span className='text-[var(--text-tertiary)] text-xs'>{language || 'code'}</span>
318-
<CopyCodeButton
319-
code={codeString}
320-
className='-mr-2 text-[var(--text-tertiary)] hover-hover:bg-[var(--surface-5)] hover-hover:text-[var(--text-secondary)]'
321-
/>
318+
<CopyCodeButton code={codeString} appearance='code-header' className='-mr-2' />
322319
</div>
323320
<div className='code-editor-theme bg-[var(--surface-5)] dark:bg-[var(--code-bg)]'>
324321
<pre className='m-0 overflow-x-auto whitespace-pre p-4 font-mono text-[var(--text-primary)] text-small leading-[21px]'>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { act } from 'react'
5+
import { createRoot } from 'react-dom/client'
6+
import { renderToStaticMarkup } from 'react-dom/server'
7+
import { describe, expect, it, vi } from 'vitest'
8+
import { CopyCodeButton } from './copy-code-button'
9+
10+
describe('CopyCodeButton', () => {
11+
it.each(['default', 'code-header'] as const)(
12+
'names the icon-only button in the %s appearance',
13+
(appearance) => {
14+
const markup = renderToStaticMarkup(<CopyCodeButton code='example' appearance={appearance} />)
15+
16+
expect(markup).toContain('aria-label="Copy code"')
17+
expect(markup).toContain('type="button"')
18+
}
19+
)
20+
21+
it('copies code, swaps to the check icon, and keeps its accessible name', async () => {
22+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
23+
const writeText = vi.fn().mockResolvedValue(undefined)
24+
const originalClipboard = Object.getOwnPropertyDescriptor(navigator, 'clipboard')
25+
Object.defineProperty(navigator, 'clipboard', {
26+
configurable: true,
27+
value: { writeText },
28+
})
29+
const host = document.createElement('div')
30+
document.body.appendChild(host)
31+
const root = createRoot(host)
32+
33+
try {
34+
await act(async () => {
35+
root.render(<CopyCodeButton code='example' appearance='code-header' />)
36+
})
37+
const button = host.querySelector('button')
38+
expect(button?.getAttribute('aria-label')).toBe('Copy code')
39+
expect(button?.querySelector('svg rect')).not.toBeNull()
40+
41+
await act(async () => {
42+
button?.click()
43+
})
44+
45+
expect(writeText).toHaveBeenCalledExactlyOnceWith('example')
46+
expect(button?.querySelector('svg rect')).toBeNull()
47+
expect(button?.querySelector('svg path')).not.toBeNull()
48+
expect(button?.getAttribute('aria-label')).toBe('Copy code')
49+
} finally {
50+
await act(async () => root.unmount())
51+
host.remove()
52+
if (originalClipboard) Object.defineProperty(navigator, 'clipboard', originalClipboard)
53+
else Reflect.deleteProperty(navigator, 'clipboard')
54+
}
55+
})
56+
})
Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,44 @@
11
'use client'
22

3+
import { cva, type VariantProps } from 'class-variance-authority'
34
import { useCopyToClipboard } from '../../hooks/use-copy-to-clipboard'
45
import { Button, Check, Duplicate } from '../../index'
56
import { cn } from '../../lib/cn'
67

8+
const copyCodeButtonVariants = cva('flex items-center gap-1 rounded px-1.5 py-0.5 text-xs', {
9+
variants: {
10+
appearance: {
11+
default: '',
12+
'code-header':
13+
'text-[var(--text-tertiary)] hover-hover:bg-[var(--surface-5)] hover-hover:text-[var(--text-secondary)]',
14+
},
15+
},
16+
defaultVariants: {
17+
appearance: 'default',
18+
},
19+
})
20+
721
interface CopyCodeButtonProps {
822
code: string
923
className?: string
24+
/** Use the muted copy action treatment in code block headers. */
25+
appearance?: VariantProps<typeof copyCodeButtonVariants>['appearance']
1026
}
1127

12-
export function CopyCodeButton({ code, className }: CopyCodeButtonProps) {
28+
export function CopyCodeButton({ code, className, appearance }: CopyCodeButtonProps) {
1329
const { copied, copy } = useCopyToClipboard()
1430

1531
return (
1632
<Button
1733
type='button'
34+
aria-label='Copy code'
1835
variant='ghost'
1936
onClick={() => copy(code)}
20-
className={cn('flex items-center gap-1 rounded px-1.5 py-0.5 text-xs', className)}
37+
className={cn(copyCodeButtonVariants({ appearance }), className)}
2138
>
2239
{copied ? <Check className='size-3.5' /> : <Duplicate className='size-3.5' />}
2340
</Button>
2441
)
2542
}
43+
44+
export { copyCodeButtonVariants }

‎packages/emcn/src/components/index.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export {
117117
calculateGutterWidth,
118118
getCodeEditorProps,
119119
} from './code/code'
120-
export { CopyCodeButton } from './code/copy-code-button'
120+
export { CopyCodeButton, copyCodeButtonVariants } from './code/copy-code-button'
121121
export { highlight, languages } from './code/prism'
122122
export { CollapsibleCard, type CollapsibleCardProps } from './collapsible-card/collapsible-card'
123123
export {

0 commit comments

Comments
 (0)