Skip to content

Commit 255d109

Browse files
authored
feat: improve copy button functionality and styling (#57)
- Refactor copy button class names for better CSS targeting - Add proper TypeScript interfaces and documentation - Improve copy button creation and positioning - Add support for custom copy button props - Update CSS selectors to match new data attributes - Fix workflow configuration
1 parent 11fab6f commit 255d109

File tree

7 files changed

+86
-50
lines changed

7 files changed

+86
-50
lines changed

.github/workflows/gh-pages.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@ on:
1010

1111
jobs:
1212
deploy:
13-
runs-on: ubuntu-20.04
13+
runs-on: ubuntu-latest
1414
permissions:
1515
contents: write
1616
concurrency:
1717
group: ${{ github.workflow }}-${{ github.ref }}
1818
steps:
19-
- uses: actions/checkout@v2
19+
- uses: actions/checkout@v4
2020

2121
- name: Setup node
22-
uses: actions/setup-node@v2
22+
uses: actions/setup-node@v4
2323
with:
2424
node-version: 'lts/*'
2525

__tests__/MarkdownRenderer-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ describe('MarkdownRenderer', () => {
6060
// Check if copy button is rendered with the correct class
6161
const copyButton = container.querySelector('button[title="Copy code"]');
6262
expect(copyButton).toBeInTheDocument();
63-
expect(copyButton).toHaveClass('btn-copy-code');
63+
expect(copyButton).toHaveAttribute('data-type', 'copy');
6464
});
6565

6666
it('applies custom copy button props', () => {

docs/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ const App = () => {
3030
renderExtraFooter={() => {
3131
return <div>Footer</div>;
3232
}}
33+
copyButtonProps={{
34+
className: 'rs-btn-icon rs-btn-icon-circle rs-btn rs-btn-subtle rs-btn-xs'
35+
}}
3336
>
3437
{example}
3538
</CodeView>

src/CopyCodeButton.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import React, { useState } from 'react';
22
import copy from 'copy-to-clipboard';
3-
import classNames from 'classnames';
43
import CopyIcon from './icons/Copy';
54
import CheckIcon from './icons/Check';
65

@@ -10,7 +9,7 @@ interface CopyCodeButtonProps extends React.ButtonHTMLAttributes<HTMLButtonEleme
109
}
1110

1211
function CopyCodeButton(props: CopyCodeButtonProps) {
13-
const { as: Component = 'button', code, className, ...rest } = props;
12+
const { as: Component = 'button', code, ...rest } = props;
1413
const [copied, setCopied] = useState(false);
1514

1615
if (!code) {
@@ -27,11 +26,7 @@ function CopyCodeButton(props: CopyCodeButtonProps) {
2726
};
2827

2928
return (
30-
<Component
31-
{...rest}
32-
className={classNames('copy-code-button', className)}
33-
onClick={handleClick}
34-
>
29+
<Component data-type="copy" onClick={handleClick} {...rest}>
3530
{copied ? <CheckIcon /> : <CopyIcon />}
3631
</Component>
3732
);

src/MarkdownRenderer.tsx

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,91 @@
1-
import React, { useEffect } from 'react';
1+
import React, { useEffect, useRef, forwardRef } from 'react';
22
import classNames from 'classnames';
33
import copy from 'copy-to-clipboard';
44
import mergeRefs from './utils/mergeRefs';
5-
import { iconPath as copyPath, svgTpl } from './icons/Copy';
6-
import { iconPath as checkPath } from './icons/Check';
5+
import { iconPath as copyIconPath, svgTpl } from './icons/Copy';
6+
import { iconPath as checkIconPath } from './icons/Check';
77

88
interface MarkdownRendererProps extends React.HTMLAttributes<HTMLDivElement> {
9+
/**
10+
* Markdown content as HTML string
11+
*/
912
children?: string | null;
13+
/**
14+
* Props to be passed to the copy button
15+
*/
1016
copyButtonProps?: React.HTMLAttributes<HTMLButtonElement>;
1117
}
1218

13-
function appendCopyButton(
19+
/**
20+
* Creates and appends a copy button to a code container
21+
* @param container - The container element to append the copy button to
22+
* @param buttonProps - Additional props to apply to the copy button
23+
*/
24+
function createCopyButton(
1425
container?: HTMLDivElement | null,
1526
buttonProps?: React.HTMLAttributes<HTMLButtonElement>
16-
) {
17-
if (!container) {
27+
): void {
28+
// If the container is null or the container already has a copy button, return
29+
if (!container || container.querySelector('button[data-type="copy"]')) {
1830
return;
1931
}
2032

33+
const { className, ...rest } = buttonProps || {};
2134
const button = document.createElement('button');
22-
button.className = 'btn-copy-code';
35+
button.dataset['type'] = 'copy';
2336
button.title = 'Copy code';
24-
button.innerHTML = svgTpl(copyPath);
37+
button.setAttribute('aria-label', 'Copy code');
38+
button.innerHTML = svgTpl(copyIconPath);
2539

26-
button.onclick = e => {
40+
if (className) {
41+
button.className = className;
42+
}
43+
44+
button.onclick = (e: MouseEvent) => {
2745
e.preventDefault();
28-
const code = container?.querySelector('code')?.textContent;
46+
const code = container.querySelector('code')?.textContent;
2947
const icon = button.querySelector('.copy-icon-path');
3048

31-
icon?.setAttribute('d', checkPath);
49+
// Show check icon to indicate successful copy
50+
icon?.setAttribute('d', checkIconPath);
51+
3252
if (code) {
3353
copy(code);
3454
}
3555

56+
// Reset to copy icon after 2 seconds
3657
setTimeout(() => {
37-
icon?.setAttribute('d', copyPath);
58+
icon?.setAttribute('d', copyIconPath);
3859
}, 2000);
3960
};
4061

41-
if (buttonProps) {
42-
Object.entries(buttonProps || {}).forEach(([key, value]) => {
43-
button.setAttribute(key, value);
62+
// Apply additional button properties
63+
if (rest) {
64+
Object.entries(rest).forEach(([key, value]) => {
65+
if (value !== undefined) {
66+
button.setAttribute(key, String(value));
67+
}
4468
});
4569
}
4670

47-
container?.appendChild(button);
71+
container.appendChild(button);
4872
}
4973

50-
const MarkdownRenderer = React.forwardRef(
74+
/**
75+
* Renders markdown content with code blocks that have copy buttons
76+
*/
77+
const MarkdownRenderer = forwardRef<HTMLDivElement, MarkdownRendererProps>(
5178
(props: MarkdownRendererProps, ref: React.Ref<HTMLDivElement>) => {
5279
const { children, className, copyButtonProps, ...rest } = props;
53-
const mdRef = React.useRef<HTMLDivElement>(null);
80+
const mdRef = useRef<HTMLDivElement>(null);
5481

5582
useEffect(() => {
56-
mdRef.current?.querySelectorAll('.rcv-code-renderer').forEach((el: any) => {
57-
appendCopyButton(el, copyButtonProps);
83+
// Add copy buttons to all code blocks
84+
const codeBlocks = mdRef.current?.querySelectorAll('.rcv-code-renderer');
85+
codeBlocks?.forEach(codeBlock => {
86+
createCopyButton(codeBlock as HTMLDivElement, copyButtonProps);
5887
});
88+
// We only want to run this once when the component mounts
5989
// eslint-disable-next-line react-hooks/exhaustive-deps
6090
}, []);
6191

src/Renderer.tsx

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,23 @@ import { transform as transformCode, Options } from 'sucrase';
1111
const React = require('react');
1212
const ReactDOM = require('react-dom');
1313

14+
interface EditorProps {
15+
/** The className of the editor */
16+
className?: string;
17+
18+
/** Add a prefix to the className of the buttons on the toolbar */
19+
classPrefix?: string;
20+
21+
/** The className of the code button displayed on the toolbar */
22+
buttonClassName?: string;
23+
24+
/** Customize the code icon on the toolbar */
25+
icon?: React.ReactNode;
26+
27+
/** The properties of the show code button */
28+
showCodeButtonProps?: React.HTMLAttributes<HTMLButtonElement>;
29+
}
30+
1431
export interface RendererProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange'> {
1532
/** Code editor theme, applied to CodeMirror */
1633
theme?: 'light' | 'dark';
@@ -28,18 +45,7 @@ export interface RendererProps extends Omit<React.HTMLAttributes<HTMLElement>, '
2845
editable?: boolean;
2946

3047
/** Editor properties */
31-
editor?: {
32-
className?: string;
33-
34-
/** Add a prefix to the className of the buttons on the toolbar */
35-
classPrefix?: string;
36-
37-
/** The className of the code button displayed on the toolbar */
38-
buttonClassName?: string;
39-
40-
/** Customize the code icon on the toolbar */
41-
icon?: React.ReactNode;
42-
};
48+
editor?: EditorProps;
4349

4450
/**
4551
* https://github.com/alangpierce/sucrase#transforms
@@ -97,6 +103,7 @@ const Renderer = React.forwardRef((props: RendererProps, ref: React.Ref<HTMLDivE
97103
icon: codeIcon,
98104
className: editorClassName,
99105
buttonClassName,
106+
showCodeButtonProps,
100107
...editorProps
101108
} = editor;
102109

@@ -168,16 +175,17 @@ const Renderer = React.forwardRef((props: RendererProps, ref: React.Ref<HTMLDivE
168175
[executeCode, onChange]
169176
);
170177

171-
const showCodeButtonProps = {
178+
const toggleButtonProps = {
172179
role: 'switch',
173180
'aria-checked': editable,
174181
'aria-label': 'Show the full source',
175182
className: buttonClassName,
176-
onClick: handleExpandEditor
183+
onClick: handleExpandEditor,
184+
...showCodeButtonProps
177185
};
178186

179187
const showCodeButton = (
180-
<button {...showCodeButtonProps}>
188+
<button {...toggleButtonProps}>
181189
{typeof codeIcon !== 'undefined' ? (
182190
codeIcon
183191
) : (
@@ -195,7 +203,7 @@ const Renderer = React.forwardRef((props: RendererProps, ref: React.Ref<HTMLDivE
195203
{compiledReactNode}
196204
</Preview>
197205
<div className="rcv-toolbar">
198-
{renderToolbar ? renderToolbar(showCodeButton, showCodeButtonProps) : showCodeButton}
206+
{renderToolbar ? renderToolbar(showCodeButton, toggleButtonProps) : showCodeButton}
199207
</div>
200208
{showCodeEditor && (
201209
<CodeEditor

src/less/styles.less

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,18 +68,18 @@
6868
}
6969
}
7070

71-
.copy-code-button {
71+
button[data-type="copy"] {
7272
position: absolute;
7373
color: #858b94;
7474
}
7575

76-
.rcv-editor .copy-code-button {
76+
.rcv-editor button[data-type="copy"] {
7777
right: 8px;
7878
top: 16px;
7979
z-index: 1;
8080
}
8181

82-
.rcv-highlight .copy-code-button {
82+
.rcv-highlight button[data-type="copy"] {
8383
right: 8px;
8484
top: 8px;
8585
}

0 commit comments

Comments
 (0)